Compare commits

...

4 Commits

  1. 1
      src/components/Container.tsx
  2. 13
      src/components/Dashed.tsx
  3. 145
      src/components/FlickerList.tsx
  4. 31
      src/components/Menu.tsx
  5. 1
      src/index.css
  6. 3
      src/pages/blog/Home.tsx
  7. 3
      src/pages/blog/components/BlogContent.tsx
  8. 2
      src/pages/main/404.tsx
  9. 6
      src/pages/main/Contact.tsx
  10. 3
      src/pages/main/Exp.tsx
  11. 89
      src/pages/main/Home.tsx
  12. 70
      src/pages/main/Projects.tsx
  13. 54
      src/pages/main/data/project.ts
  14. 1
      src/util/dynamic-gradient.css
  15. 26
      src/util/index.ts

1
src/components/Container.tsx

@ -157,7 +157,6 @@ const Container: React.FC<{
return ( return (
<div <div
className={css` className={css`
background: var(--bg-colour);
padding-block-start: 15rem; padding-block-start: 15rem;
padding-block-end: 8rem; padding-block-end: 8rem;
padding-inline: calc(100vw / 8); padding-inline: calc(100vw / 8);

13
src/components/Dashed.tsx

@ -1,13 +0,0 @@
import React from "react";
import { css } from "@emotion/css";
const Dashed: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<span
className={css`
border-bottom: 1px dashed var(--text-colour);
`}>
{children}
</span>
);
export default Dashed;

145
src/components/FlickerList.tsx

@ -0,0 +1,145 @@
import React from "react";
import { css, cx } from "@emotion/css";
import { intersperse, sleep } from "../util";
const tripleBlink = async (el: HTMLElement) => {
const delay = 150;
await sleep(1000);
el.style.opacity = "0.5";
await sleep(delay);
el.style.opacity = "1";
await sleep(delay);
el.style.opacity = "0.5";
await sleep(delay);
el.style.opacity = "1";
await sleep(delay);
el.style.opacity = "0.5";
await sleep(delay * 2);
el.style.opacity = "1";
};
const Flicker: React.FC<{
children: React.ReactNode;
index: number;
description: string;
}> = ({ children, index, description }) => {
return (
<span
className={css`
position: relative;
& button:focus ~ .tooltip,
& button:hover ~ .tooltip {
opacity: 1;
}
& button:focus,
& button:hover {
opacity: 1 !important;
}
`}>
<button
className={css`
border-bottom: 1px dashed var(--text-colour);
opacity: 0;
background-color: transparent;
border: none;
color: inherit;
position: relative;
`}
ref={async el => {
if (!el) return;
await sleep(150);
await sleep(250 * index);
el.style.opacity = "1";
await sleep(1000 + Math.random() * 1000);
tripleBlink(el);
while (true) {
await sleep(5000 + Math.random() * 10000);
el.style.opacity = String(0.5 + Math.random() * 0.5);
await sleep(2000);
tripleBlink(el);
}
}}>
{children}
</button>
<span
className={cx(
"tooltip",
css`
/* tooltip */
position: absolute;
top: 150%;
left: 50%;
transform: translateX(-50%);
background: var(--card-tags);
color: var(--text-colour);
border-radius: 0.5rem;
padding: 0.5rem 0.8rem;
font-size: 0.8rem;
min-width: 20rem;
width: fit-content;
max-width: 80vw;
opacity: 0;
transition: opacity 0.2s;
user-select: none;
text-align: left;
@media screen and (max-width: 65rem) {
position: fixed;
top: unset;
left: 1rem;
transform: translateY(2rem);
}
`,
)}>
{description}
</span>
</span>
);
};
const FlickerList: React.FC<{
list: { text: string; description: string }[];
}> = ({ list }) => {
return (
<ul
className={css`
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
&:has(> li > span > button:focus) li > span > button:not(:focus),
&:has(> li > span > button:hover) li > span > button:not(:hover)
/* */ {
opacity: 0.5 !important;
}
`}>
{[
...intersperse(
list.map((item, index) => (
<li
key={item.text}
className={css`
display: inline-block;
`}>
<Flicker index={index} description={item.description}>
{item.text}
</Flicker>
</li>
)),
<li>·</li>,
),
]}
</ul>
);
};
export default FlickerList;

31
src/components/Menu.tsx

@ -43,6 +43,15 @@ const menuList = css`
align-items: center; align-items: center;
font-weight: 800; font-weight: 800;
& a.active {
color: var(--primary-colour);
/* text-decoration: underline; */
}
& a:hover {
text-decoration: underline;
}
& > li { & > li {
margin-left: 1rem; margin-left: 1rem;
} }
@ -68,16 +77,24 @@ const Menu: React.FC<{
show?: boolean; show?: boolean;
setShowMenu: (show: boolean) => void; setShowMenu: (show: boolean) => void;
}> = ({ show = false, setShowMenu }) => { }> = ({ show = false, setShowMenu }) => {
const navigate = useNav(); const [location, 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 = Object.entries(MENU).map(([name, link]) => ( const menuItems = Object.entries(MENU).map(([name, link]) => {
<a key={link} onClick={navigate(link)} href={link}> const active = location.split("/")[1] === link.split("/")[1];
{name}
</a> return (
)); <a
className={cx({ active })}
key={link}
onClick={navigate(link)}
href={link}>
{name}
</a>
);
});
return ( return (
<motion.div <motion.div
@ -89,7 +106,7 @@ const Menu: React.FC<{
// only children need to animate on desktop, lock opacity: 1 // only children need to animate on desktop, lock opacity: 1
opacity: notmobile ? 1 : show ? 1 : 0, opacity: notmobile ? 1 : show ? 1 : 0,
}}> }}>
<ul className={notmobile ? menuList : cx(menuList, mobileMenu)}> <ul className={cx(menuList, !notmobile && mobileMenu)}>
<RevealChildren type="li" show={show}> <RevealChildren type="li" show={show}>
{notmobile {notmobile
? menuItems ? menuItems

1
src/index.css

@ -27,6 +27,7 @@ body {
margin: 0; margin: 0;
color: var(--text-colour); color: var(--text-colour);
overflow-x: hidden; overflow-x: hidden;
background-color: var(--bg-colour);
} }
code { code {

3
src/pages/blog/Home.tsx

@ -58,8 +58,7 @@ const Header: React.FC = () => {
}; };
const BlogHome: React.FC = () => { const BlogHome: React.FC = () => {
const [location] = useLocation(); const [location, navigate] = useNav();
const navigate = useNav();
const isArticleOpen = Boolean(location.split("/blog")[1]); const isArticleOpen = Boolean(location.split("/blog")[1]);
const [isAsideClosed, setAsideClosed] = useState(isArticleOpen); const [isAsideClosed, setAsideClosed] = useState(isArticleOpen);

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

@ -112,8 +112,7 @@ const btn = css`
`; `;
export const BlogPost: React.FC = () => { export const BlogPost: React.FC = () => {
const navigate = useNav(); const [location, navigate] = useNav();
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);

2
src/pages/main/404.tsx

@ -3,7 +3,7 @@ import Container from "../../components/Container";
import { useNav } from "../../util"; import { useNav } from "../../util";
function Home() { function Home() {
const navigate = useNav(); const [, navigate] = useNav();
return ( return (
<Container> <Container>

6
src/pages/main/Contact.tsx

@ -36,6 +36,7 @@ const CONTACT: Contact = {
Γ: "7", Γ: "7",
}, },
}, },
"Blog": { value: "→", link: "https://MKRhere.com" },
}; };
const Home: React.FC = () => { const Home: React.FC = () => {
@ -110,6 +111,11 @@ const Home: React.FC = () => {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
/* Blog entry */
li:last-child {
margin-block-start: 1rem;
}
} }
`}> `}>
<ul <ul

3
src/pages/main/Exp.tsx

@ -66,7 +66,8 @@ const Exp: React.FC = () => {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, 20rem); grid-template-columns: repeat(auto-fit, 20rem);
gap: 1rem; row-gap: 0.5rem;
column-gap: 1rem;
@media screen and (min-width: ${offscreenWidth}) { @media screen and (min-width: ${offscreenWidth}) {
transform: translateX(0); transform: translateX(0);

89
src/pages/main/Home.tsx

@ -1,15 +1,92 @@
import React from "react"; import React from "react";
import Container from "../../components/Container"; import Container from "../../components/Container";
import Dashed from "../../components/Dashed"; import FlickerList from "../../components/FlickerList";
import { ReactComponent as Arrow } from "../../assets/arrow-thin.svg";
import { css, cx } from "@emotion/css";
import { setupCursorTracking } from "../../util";
const section = css`
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
`;
const Home: React.FC = () => { const Home: React.FC = () => {
return ( return (
<Container> <Container>
<h1>MKRhere</h1> <section>
<p> <h1>MKRhere</h1>
Web home of <Dashed>designer</Dashed>, <Dashed>developer</Dashed>, and{" "} <p>
<Dashed>architect</Dashed> <b>Muthu Kumar.</b> <FlickerList
</p> list={[
{
text: "Designer",
description:
"Graphic design is my passion 🤓 I have plenty of experience with Figma and Adobe Suite tools (especially Photoshop and InDesign)",
},
{
text: "Developer",
description:
"🧑🏻‍💻 I started developing websites in 2015, and in 2017 I joined The Devs Network, catapulting my growth as a full-time developer",
},
{
text: "Architect",
description:
"I have a formal degree in architecture! I'm an architect in both construction and software 😉",
},
]}
/>
</p>
</section>
<section
className={cx(
section,
css`
gap: 0.2rem;
`,
)}>
<p>
Welcome to the web home of <b>Anu Rahul Nandhan.</b>
</p>
<p>
I'm also commonly known as <b>Muthu Kumar</b>.
</p>
</section>
<section className={section}>
<p>I'm looking for work! 🎉</p>
<button
className={css`
background: var(--card-tags);
border: 0;
border-radius: 0.5rem;
width: fit-content;
color: var(--text-colour);
cursor: pointer;
font-size: 1rem;
position: relative;
z-index: 0;
& a {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 2rem;
}
& a:hover {
color: inherit;
}
`}
ref={setupCursorTracking}>
<div className="dynamic-gradient" />
<a href="https://mkr.pw/resume" target="_blank">
Download Resume
<Arrow />
</a>
</button>
</section>
</Container> </Container>
); );
}; };

70
src/pages/main/Projects.tsx

@ -1,61 +1,7 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { css } from "@emotion/css";
import Container from "../../components/Container"; import Container from "../../components/Container";
import { projects } from "./data/project";
const exp = [
{
title: window.location.hostname,
description: "This website.",
url: "https://github.com/MKRhere/pw2",
cat: "web",
tags: ["react", "vite"],
},
{
title: "hyperactive",
description: "Suite of web-app development libraries.",
url: "https://github.com/codefeathers/hyperactive",
cat: "lib",
tags: ["reactive", "ui-framework"],
},
{
title: "denoland/node_shims",
description:
"Node shims for Deno’s runtime API. Contributed repo into official denoland.",
url: "https://github.com/denoland/node_shims",
cat: "lib",
tags: ["deno", "shims"],
},
{
title: "Telegraf",
description:
"Active maintainer of one of the most popular Telegram Bot API libraries for Node.",
url: "https://github.com/telegraf/telegraf",
cat: "lib",
tags: ["typescript", "telegram", "bot-api"],
},
{
title: "runtype",
description: "Safely bring runtime values into TypeScript.",
url: "https://codefeathers.github.io/runtype",
cat: "lib",
tags: ["typescript", "runtime"],
},
{
title: "Telecraft",
description: "Pluggable Minecraft server administration toolkit.",
url: "https://github.com/MadrasMC/telecraft",
cat: "tool",
tags: ["minecraft", "node"],
},
{
title: "Storymap",
description:
"Reverse-engineered thirdparty map renderer for Vintage Story in Zig ⚡️.",
// url: "https://github.com/MadrasMC/storymap",
cat: "tool",
tags: ["vintage-story", "zig"],
},
];
type Project = { type Project = {
title: string; title: string;
@ -104,6 +50,7 @@ const ProjectUnit: React.FC<Project> = ({
className={css` className={css`
color: #bdbdbd; color: #bdbdbd;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
font-size: 0.9rem;
`}> `}>
{description} {description}
</p> </p>
@ -159,17 +106,12 @@ const Exp: React.FC = () => {
<p>Some tools, libraries, and apps over time:</p> <p>Some tools, libraries, and apps over time:</p>
<div <div
className={css` className={css`
display: flex; display: grid;
grid-template-columns: repeat(auto-fit, 20rem);
gap: 1rem;
width: 100%; width: 100%;
flex-wrap: wrap;
gap: 2rem;
& > * {
flex-basis: 15rem;
flex-grow: 1;
}
`}> `}>
{exp.map(unit => ( {projects.map(unit => (
<ProjectUnit {...unit} key={unit.title} /> <ProjectUnit {...unit} key={unit.title} />
))} ))}
</div> </div>

54
src/pages/main/data/project.ts

@ -0,0 +1,54 @@
export const projects = [
{
title: window.location.hostname,
description: "This website.",
url: "https://github.com/MKRhere/pw2",
cat: "web",
tags: ["react", "vite"],
},
{
title: "hyperactive",
description: "Suite of web-app development libraries.",
url: "https://github.com/codefeathers/hyperactive",
cat: "lib",
tags: ["reactive", "ui-framework"],
},
{
title: "denoland/node_shims",
description:
"Node shims for Deno’s runtime API. Contributed repo into official denoland.",
url: "https://github.com/denoland/node_shims",
cat: "lib",
tags: ["deno", "shims"],
},
{
title: "Telegraf",
description:
"Active maintainer of one of the most popular Telegram Bot API libraries for Node.",
url: "https://github.com/telegraf/telegraf",
cat: "lib",
tags: ["typescript", "telegram", "bot-api"],
},
{
title: "runtype",
description: "Safely bring runtime values into TypeScript.",
url: "https://codefeathers.github.io/runtype",
cat: "lib",
tags: ["typescript", "runtime"],
},
{
title: "Telecraft",
description: "Pluggable Minecraft server administration toolkit.",
url: "https://github.com/MadrasMC/telecraft",
cat: "tool",
tags: ["minecraft", "node"],
},
{
title: "Storymap (WIP)",
description:
"Reverse-engineered thirdparty map renderer for Vintage Story in Zig ⚡️",
// url: "https://github.com/MadrasMC/storymap",
cat: "tool",
tags: ["vintage-story", "zig", "wip"],
},
];

1
src/util/dynamic-gradient.css

@ -22,7 +22,6 @@
z-index: -1; z-index: -1;
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
transform-origin: --size, --size;
scale: 0; scale: 0;
opacity: 0; opacity: 0;
background: radial-gradient( background: radial-gradient(

26
src/util/index.ts

@ -1,6 +1,17 @@
import React from "react"; import React from "react";
import useLocation from "wouter/use-location"; import useLocation from "wouter/use-location";
export const sleep = (t: number) => new Promise(r => setTimeout(r, t));
export function* intersperse<T, U>(xs: T[], delim: U): Generator<T | U> {
let first = true;
for (const x of xs) {
if (!first) yield delim;
first = false;
yield x;
}
}
export const getTimeout = () => { export const getTimeout = () => {
const clearables = new Set<number>(); const clearables = new Set<number>();
@ -21,13 +32,16 @@ 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] = useLocation(); const [location, navigate] = useLocation();
return (link: string) => (e: React.MouseEvent | KeyboardEvent) => { return [
e?.preventDefault(); location,
if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener"); (link: string) => (e: React.MouseEvent | KeyboardEvent) => {
navigate(link); e?.preventDefault();
}; if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener");
navigate(link);
},
] as const;
}; };
export function rewriteExtn(filename: string, extn: string) { export function rewriteExtn(filename: string, extn: string) {

Loading…
Cancel
Save