Compare commits

...

6 Commits

  1. 2
      package.json
  2. 3950
      pnpm-lock.yaml
  3. 3
      src/assets/cross.svg
  4. 8
      src/components/Container.tsx
  5. 35
      src/components/Exp/Content.tsx
  6. 167
      src/components/Exp/Story.tsx
  7. 124
      src/components/Exp/Tags.tsx
  8. 39
      src/components/Exp/Unit.tsx
  9. 1
      src/components/constants.ts
  10. 2
      src/index.css
  11. 26
      src/index.tsx
  12. 95
      src/pages/main/Exp.tsx
  13. 17
      src/pages/main/Projects.tsx
  14. 285
      src/pages/main/data/experience.tsx
  15. 57
      src/util/useSearchParams.ts

2
package.json

@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite --host",
"build": "pnpm blog && tsc && vite build", "build": "pnpm blog && tsc && vite build",
"serve": "vite preview", "serve": "vite preview",
"blog": "node scripts/blog.js" "blog": "node scripts/blog.js"

3950
pnpm-lock.yaml

File diff suppressed because it is too large

3
src/assets/cross.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none">
<path d="M18 6L12 12M12 12L6 18M12 12L18 18M12 12L6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 252 B

8
src/components/Container.tsx

@ -211,7 +211,6 @@ const Container: React.FC<{
border-radius: 100%; border-radius: 100%;
border: 0; border: 0;
background: none; background: none;
box-shadow: 0 0 1rem 0 rgba(100, 100, 100, 0.5);
cursor: pointer; cursor: pointer;
& > svg { & > svg {
@ -308,10 +307,9 @@ const Container: React.FC<{
max-width: 62rem; max-width: 62rem;
min-height: 100%; min-height: 100%;
margin: auto; margin: auto;
display: flex;
& > * { flex-direction: column;
margin-bottom: 2rem; gap: 2rem;
}
`, `,
className, className,
)} )}

35
src/components/Exp/Content.tsx

@ -1,9 +1,9 @@
import { css } from "@emotion/css"; import { css, cx } from "@emotion/css";
import React from "react"; import React from "react";
import { setupCursorTracking } from "../../util"; import { setupCursorTracking } from "../../util";
import { Experience } from "./types"; import { Experience } from "./types";
const Circle: React.FC = () => ( const TimelineSegment: React.FC = () => (
<div className="timeline-segment" aria-hidden> <div className="timeline-segment" aria-hidden>
<div <div
className={css` className={css`
@ -12,21 +12,25 @@ const Circle: React.FC = () => (
background: #333333; background: #333333;
left: -50vw; left: -50vw;
position: absolute; position: absolute;
top: calc(-3rem + 0.2rem / 2); top: calc(-3rem + 0.3rem / 2);
transform: translateY(-50%);
z-index: 0; z-index: 0;
`}></div> `}></div>
<div <div
className={css` className={cx(
width: 0.25rem; "timeline-circle",
height: 0.25rem; css`
background: #333333; width: 0.3rem;
background: #ffffff; height: 0.3rem;
border-radius: 100%; background: #333333;
position: absolute; border-radius: 100%;
top: -3rem; position: absolute;
left: 0; top: -3rem;
z-index: 100; left: 0;
`}></div> z-index: 100;
transition: background var(--transition-time) ease-in-out;
`,
)}></div>
</div> </div>
); );
@ -56,7 +60,6 @@ const btn = css`
@media (pointer: fine) { @media (pointer: fine) {
&:hover { &:hover {
background-color: var(--card-hover); background-color: var(--card-hover);
z-index: 1000;
box-shadow: 0 0 25rem 2rem rgba(190, 190, 190, 0.1); box-shadow: 0 0 25rem 2rem rgba(190, 190, 190, 0.1);
} }
} }
@ -92,7 +95,7 @@ export const Content = ({
return ( return (
<button className={btn} onClick={onClick} ref={setupCursorTracking}> <button className={btn} onClick={onClick} ref={setupCursorTracking}>
<div className="dynamic-gradient" /> <div className="dynamic-gradient" />
<Circle /> <TimelineSegment />
<h4> <h4>
{title} {title}
<span className="year"> · ({year})</span> <span className="year"> · ({year})</span>

167
src/components/Exp/Story.tsx

@ -1,6 +1,8 @@
import React from "react"; import React from "react";
import { css, cx } from "@emotion/css"; import { css, cx } from "@emotion/css";
import { ReactComponent as Close } from "../../assets/close.svg";
import { Experience } from "./types"; import { Experience } from "./types";
import { offscreenWidth } from "../constants";
const story = css` const story = css`
position: absolute; position: absolute;
@ -9,43 +11,135 @@ const story = css`
border-radius: 0.5rem; border-radius: 0.5rem;
display: flex; display: flex;
overflow: hidden; & * {
line-height: 140%;
& .contents { }
padding: 1.5rem;
line-height: 1.25rem;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 2rem;
margin-block-start: 1rem;
height: var(--story-height);
& ul { & .story-content {
& > div {
max-height: 100%; max-height: 100%;
margin: 0; margin: 0;
column-count: 3; column-count: 3;
column-gap: 2rem; column-gap: 2.5rem;
color: var(--text-subdued); color: var(--text-subdued);
font-weight: 400; font-weight: 400;
& li + li { & p {
margin-block-start: 0.5rem; font-size: 0.9rem;
}
& > p + p {
margin-block-start: 1.4em;
} }
}
}
/* desktop & tablet */
@media screen and (min-width: ${offscreenWidth}) {
/* offset padding */
transform: translateX(calc(var(--item-padding) * -1));
& .story-handle {
display: none;
}
& .story-content {
padding: 1.5rem;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 3rem;
margin-block-start: 1rem;
height: var(--story-height);
}
}
/* mobile */
@media screen and (max-width: ${offscreenWidth}) {
position: fixed;
display: flex;
justify-content: center;
height: calc(100vh - 10rem);
width: 100vw;
left: 0;
z-index: 900;
font-size: 1.25rem;
background: var(--bg-colour);
border-inline: 1px solid var(--offscreen-handle);
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
/*
Push this offscreen element out of screen;
Unit.ts will pull it up when .active
*/
top: 100vh;
& .story-handle {
content: "";
width: 100%;
height: 1rem;
background-color: var(--offscreen-handle);
position: absolute;
top: 0;
left: 0;
& li::marker { &::before {
content: ""; content: "";
font-weight: 800; position: absolute;
padding-top: 1rem; width: 2rem;
height: 0.2rem;
background-color: var(--offscreen-handle-tab);
z-index: 900;
inset: 0;
top: 0;
margin: auto;
}
}
& .story-content {
display: flex;
flex-direction: column;
gap: 3rem;
/* height: 100%; */
max-width: 40rem;
margin-block: 4rem;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
& > img {
margin-inline: 4rem;
}
& > div {
column-count: unset;
padding-inline: 4rem;
padding-block-end: 3rem;
height: 100%;
overflow-y: auto;
& p {
font-size: 1rem;
}
}
& > .closer {
display: flex;
} }
} }
} }
`; `;
export const Story = ({ description, logo }: Experience) => { export const Story = ({ description, logo, active }: Experience) => {
return ( return (
<div className={cx(story, "story")}> <div
<div className="contents"> className={cx(story, "story")}
id={active ? "active-story" : undefined}>
<div aria-hidden className="story-handle" />
<div className="story-content">
<img <img
src={`/assets/logos/` + logo} src={`/assets/logos/` + logo}
className={cx( className={cx(
@ -59,7 +153,36 @@ export const Story = ({ description, logo }: Experience) => {
`, `,
)} )}
/> />
<ul>{description}</ul> <button
className={cx(
"closer",
css`
display: none;
appearance: none;
border: none;
background: var(--card-active);
color: var(--text-subdued);
width: 3rem;
height: 3rem;
border-radius: 3rem;
/* set in mobile mode */
/* display: flex; */
justify-content: center;
align-items: center;
position: absolute;
top: 4rem;
right: 4rem;
cursor: pointer;
transform: rotate(90deg);
`,
)}
onClick={() => window.history.back()}>
<Close />
</button>
<div>{description}</div>
</div> </div>
</div> </div>
); );

124
src/components/Exp/Tags.tsx

@ -0,0 +1,124 @@
import React from "react";
import { css, cx } from "@emotion/css";
import { HookSet } from "../../util/useSearchParams";
import { ReactComponent as Cross } from "../../assets/cross.svg";
type Tags = HookSet<string>;
const tag = css`
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem 0.9rem 0.5rem 0.6rem;
font-size: 0.85rem;
background-color: transparent;
border: 1px solid var(--card-bg);
color: var(--text-colour);
transition: background-color 150ms;
display: flex;
align-items: center;
gap: 0.5rem;
&:hover {
background-color: var(--card-tags-hover);
}
&.active {
background-color: var(--card-tags);
}
`;
export const Tag = (props: { tag: string; selected: Tags }) => {
const { selected } = props;
const active = selected.has(props.tag);
const select = () => {
if (selected.has(props.tag)) selected.remove(props.tag);
else selected.add(props.tag);
};
return (
<button className={cx(tag, { active })} onClick={select}>
<span
className={cx(
css`
transition: transform 100ms ease-in-out;
overflow: hidden;
transform: rotate(45deg);
width: 0.85rem;
&.active {
transform: rotate(0deg);
}
`,
{ active },
)}>
<Cross
className={css`
display: block;
height: 0.85rem;
width: 0.85rem;
fill: var(--text-colour);
`}
/>
</span>
{props.tag}
</button>
);
};
const clear = css`
border: none;
color: var(--primary-colour);
opacity: 1;
transition: opacity 200ms;
&.hide {
opacity: 0;
user-select: none;
cursor: unset;
height: 0;
}
&:hover {
background-color: transparent;
}
`;
export const Clear = (props: { selected: Tags }) => {
const { selected } = props;
return (
<button
className={cx(tag, clear, { hide: !selected.size })}
onClick={selected.clear}>
<Cross
className={css`
display: block;
height: 0.85rem;
width: 0.85rem;
fill: var(--text-colour);
`}
/>
Clear filters
</button>
);
};
export const Tags = (props: { tags: string[]; selected: Tags }) => {
const { tags, selected } = props;
return (
<div
className={css`
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
`}>
{tags.map(tag => (
<Tag key={tag} tag={tag} selected={selected} />
))}
<Clear selected={selected} />
</div>
);
};

39
src/components/Exp/Unit.tsx

@ -3,24 +3,25 @@ import { css, cx } from "@emotion/css";
import { Story } from "./Story"; import { Story } from "./Story";
import { Experience } from "./types"; import { Experience } from "./types";
import { Content } from "./Content"; import { Content } from "./Content";
import { offscreenWidth } from "../constants";
const expUnit = css` const expUnit = css`
--final-height: 20rem; --final-height: 24rem;
--unit-height: 9rem; --unit-height: 9rem;
--story-height: calc(var(--final-height) - var(--unit-height)); --story-height: calc(var(--final-height) - var(--unit-height));
--transition-time: 300ms; --transition-time: 200ms;
& > * { & > * {
line-height: 1em; line-height: 1em;
font-size: 1rem; font-size: 1rem;
} }
& button { & > button {
border: 1px solid transparent; border: 1px solid transparent;
transition: all calc(var(--transition-time) * 2); transition: all calc(var(--transition-time) * 2);
} }
&.active button { &.active > button {
background-color: var(--card-active); background-color: var(--card-active);
border: 1px solid var(--card-active-border); border: 1px solid var(--card-active-border);
box-shadow: 0 0 50rem 0 rgba(190, 190, 190, 0.5); box-shadow: 0 0 50rem 0 rgba(190, 190, 190, 0.5);
@ -41,19 +42,34 @@ const expUnit = css`
& .story { & .story {
opacity: 0; opacity: 0;
transition: opacity var(--transition-time) ease-in-out; transition: all calc(var(--transition-time)) ease-in-out;
transition-delay: 0; transition-delay: 0ms;
} }
&.active { &.active {
height: var(--final-height); transition-delay: 0ms;
transition-delay: 0;
transition-delay: var(--transition-time); .timeline-circle {
background: #ffffff;
}
.story { .story {
transition-delay: var(--transition-time);
opacity: 1; opacity: 1;
transition: opacity calc(var(--transition-time) * 2) ease-in-out; }
@media screen and (min-width: ${offscreenWidth}) {
transition-delay: var(--transition-time); transition-delay: var(--transition-time);
height: var(--final-height);
}
@media screen and (max-width: ${offscreenWidth}) {
.story {
transition-delay: 0ms;
position: fixed;
inset: 0;
top: 10rem;
}
} }
} }
@ -61,8 +77,9 @@ const expUnit = css`
`; `;
export const ExpUnit = (props: Experience) => { export const ExpUnit = (props: Experience) => {
const { active } = props;
return ( return (
<div className={cx(expUnit, { active: props.active })}> <div className={cx(expUnit, { active })}>
<Content {...props} /> <Content {...props} />
<Story {...props} /> <Story {...props} />
</div> </div>

1
src/components/constants.ts

@ -0,0 +1 @@
export const offscreenWidth = "82rem";

2
src/index.css

@ -64,7 +64,7 @@ h1 {
} }
h2 { h2 {
font-size: min(10vw, 3rem); font-size: min(8vw, 3rem);
} }
h3 { h3 {

26
src/index.tsx

@ -25,23 +25,15 @@ function App() {
return null; return null;
} }
switch (normalised) { if (normalised === "/") return <Home />;
case "/": if (normalised === "/experience") return <Exp />;
return <Home />; if (normalised.startsWith("/experience/")) return <Exp />;
case "/experience": if (normalised === "/projects") return <Projects />;
return <Exp />; if (normalised === "/contact") return <Contact />;
case "/projects": if (normalised === "/live") return <Live />;
return <Projects />; if (normalised === "/blog") return <BlogHome />;
case "/contact": if (location.startsWith("/blog")) return <BlogPost />;
return <Contact />; return <NotFound />;
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(

95
src/pages/main/Exp.tsx

@ -1,15 +1,33 @@
import React, { useEffect, useState } from "react"; import React, { useEffect } from "react";
import { css } from "@emotion/css"; import { css, cx } from "@emotion/css";
import Container from "../../components/Container"; import Container from "../../components/Container";
import { ExpUnit } from "../../components/Exp/Unit"; import { ExpUnit } from "../../components/Exp/Unit";
import { age, experience } from "./data/experience"; import { age, experience } from "./data/experience";
import { offscreenWidth } from "../../components/constants";
import { Tags } from "../../components/Exp/Tags";
import useSearchParams from "../../util/useSearchParams";
import useLocation from "wouter/use-location";
const exp_route = /^\/experience\/?[^\/]*$/;
const slug_replace = /^\/experience\/?/;
const Exp: React.FC = () => { const Exp: React.FC = () => {
const [active, setActive] = useState(-1); const [location, navigate] = useLocation();
const tags = useSearchParams("tag");
if (!exp_route.test(location)) {
navigate("/experience", { replace: true });
return null;
}
const slug = location.replace(slug_replace, "").replace("/", "");
useEffect(() => { useEffect(() => {
const handler = (e: KeyboardEvent) => { const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") setActive(-1); const slug = window.location.pathname
.replace(slug_replace, "")
.replace("/", "");
if (slug) if (e.key === "Escape") window.history.back();
}; };
window.addEventListener("keydown", handler); window.addEventListener("keydown", handler);
@ -34,27 +52,56 @@ const Exp: React.FC = () => {
</span> </span>
: :
</p> </p>
<section>
<Tags
tags={["Programming", "Design", "Architecture", "Writing"]}
selected={tags}
/>
</section>
<div <div
className={css` className={cx(
width: 100%; css`
--item-padding: 1.2rem; width: 100%;
--item-padding: 1.2rem;
display: grid;
grid-template-columns: repeat(auto-fit, 20rem); display: grid;
gap: 1rem; grid-template-columns: repeat(auto-fit, 20rem);
gap: 1rem;
& > * {
padding-top: 3rem; @media screen and (min-width: ${offscreenWidth}) {
} transform: translateX(0);
`}> }
{experience.map((unit, i) => (
<ExpUnit & > * {
key={i} padding-top: 2.4rem;
active={i === active} }
{...unit} `,
onClick={() => setActive(active === i ? -1 : i)} )}>
/> {experience
))} .filter(unit => !tags.size || unit.tags.some(tag => tags.has(tag)))
.map((unit, i) => (
<ExpUnit
key={i}
active={slug === unit.slug}
{...unit}
onClick={e => {
if (slug === unit.slug) return navigate("/experience");
if (slug)
navigate(`/experience/${unit.slug}`, { replace: true });
else navigate(`/experience/${unit.slug}`);
// setTimeout(() => {
// console.log("dping");
// (
// (e.target as HTMLElement).nextSibling as HTMLElement
// )?.scrollIntoView?.({
// behavior: "smooth",
// block: "center",
// });
// }, 300);
}}
/>
))}
</div> </div>
</Container> </Container>
); );

17
src/pages/main/Projects.tsx

@ -47,11 +47,19 @@ const exp = [
cat: "tool", cat: "tool",
tags: ["minecraft", "node"], 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;
url: string; url?: string;
description: string; description: string;
cat: string; cat: string;
tags: string[]; tags: string[];
@ -86,6 +94,7 @@ const ProjectUnit: React.FC<Project> = ({
className={css` className={css`
display: block; display: block;
text-decoration: none; text-decoration: none;
font-weight: 500;
`} `}
href={url} href={url}
target="_blank" target="_blank"
@ -133,7 +142,6 @@ const ProjectUnit: React.FC<Project> = ({
position: absolute; position: absolute;
right: 1rem; right: 1rem;
bottom: 1rem; bottom: 1rem;
font-weight: bold;
color: #bbbbbb; color: #bbbbbb;
font-size: 0.8rem; font-size: 0.8rem;
`}> `}>
@ -147,19 +155,18 @@ const ProjectUnit: React.FC<Project> = ({
const Exp: React.FC = () => { const Exp: React.FC = () => {
return ( return (
<Container> <Container>
<h2>What else have I built?</h2> <h2>Things I've built</h2>
<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: flex;
width: 100%; width: 100%;
flex-wrap: wrap; flex-wrap: wrap;
gap: 2rem;
& > * { & > * {
flex-basis: 15rem; flex-basis: 15rem;
flex-grow: 1; flex-grow: 1;
margin-top: 2rem;
margin-right: 3%;
} }
`}> `}>
{exp.map(unit => ( {exp.map(unit => (

285
src/pages/main/data/experience.tsx

@ -1,118 +1,337 @@
import React from "react"; import React from "react";
const sampleText = `
This is gonna have some text, probably a story about what I did here.
Apollonius of Perga rich in heavy atoms great turbulent clouds citizens of distant epochs the only home we've ever known hydrogen atoms?
Bits of moving fluff two ghostly white figures in coveralls and helmets are softly dancing a still more glorious dawn awaits hearts of the stars extraordinary claims require extraordinary evidence a very small stage in a vast cosmic arena.
`;
export const experience = [ export const experience = [
{ {
title: "The Feathers", title: "The Feathers",
slug: "thefeathers",
location: "Chennai (formerly Tirunelveli, Ooty)", location: "Chennai (formerly Tirunelveli, Ooty)",
position: "Founder (Creative Collective)", position: "Founder (Creative Collective)",
year: "2011-19", year: "2011-19",
tags: ["Programming", "Film", "Photography", "Design", "Writing"],
description: ( description: (
<> <>
<li> <p>
The Feathers was a creative collective, comprising mostly of students The Feathers was a creative collective primarily consisting of
and amateur artists and entertainers pursuing various creative students and amateur artists and entertainers who were exploring a
endeavours. diverse range of creative fields. These fields spanned from
</li> photography, typography, and graphic design to short films, music,
<li> writing, programming, advertising films, and event coverage.
This varied from photography, typography, graphic design, short films, </p>
music, to ad films and event coverage. <p>
</li> This collective represented an early and earnest attempt at creating
<li> something driven purely by passion. It led to the development of
It was an early attempt at creating something out of pure passion, and various projects, including StudioFlicks and OutFocus, among others.
gave rise to such projects as StudioFlicks, OutFocus, and the like. </p>
</li> <p>
<li> The collaborative spirit and shared dedication among members fostered
It gives me immense pride and joy to see former members go on to build a nurturing environment for growth and innovation. I feel immense
amazing careers. pride and joy in seeing former members go on to build remarkable
</li> careers and continue to pursue their creative interests with such
enthusiasm and success.
</p>
</> </>
), ),
logo: "TheFeathers.png", logo: "TheFeathers.png",
}, },
{ {
title: "StudioFlicks", title: "StudioFlicks",
slug: "studioflicks",
location: "Remote (Coimbatore)", location: "Remote (Coimbatore)",
position: "Co-founder & Creative Head", position: "Co-founder & Creative Head",
year: "2013-15", year: "2013-15",
description: sampleText, tags: ["Design", "Writing"],
description: (
<>
<p>
I co-founded StudioFlicks with Suresh after working together in
writing for another cinema news website that has since went down. I
was involved in the design and content creation for the website. We
covered movie reviews, news, and interviews.
</p>
<p>
We had a small team of writers and photographers who contribute to the
website. We had a good run for a couple of years before I moved on to
other projects. I designed the logo and stationery, and wrote a lot of
the content for the website. I also managed the social media accounts
and the community.
</p>
</>
),
logo: "StudioFlicks.png", logo: "StudioFlicks.png",
}, },
{ {
title: "Vinzas", title: "Vinzas",
slug: "vinzas",
location: "Chennai", location: "Chennai",
position: "Architectural Intern", position: "Architectural Intern",
year: "2014", year: "2014",
description: sampleText, tags: ["Architecture"],
description: (
<>
<p>
I interned at Vinzas Solutions, a small but dynamic architectural firm
in Chennai, where I was involved in the design and execution of
several residential and commercial projects. This experience allowed
me to gain invaluable insights into the practical aspects of
architecture, working on projects from concept to completion.
</p>
<p>
This was my first experience working in a professional setting, and it
proved to be an exceptional learning opportunity. I learned
extensively about the design process, client interactions, and project
management, which significantly enhanced my understanding of the
industry.
</p>
<p>
Additionally, I became proficient in using various design software and
tools to create detailed drawings and presentations, further honing my
technical skills and creative abilities.
</p>
</>
),
logo: "Vinzas.png", logo: "Vinzas.png",
}, },
{ {
title: "Blue Cube", title: "Blue Cube",
slug: "bluecube",
location: "Chennai", location: "Chennai",
position: "Architectural Intern", position: "Architectural Intern",
year: "2015", year: "2015",
description: sampleText, tags: ["Architecture"],
description: (
<>
<p>
I interned at Blue Cube, a leading architectural firm in Chennai,
where I worked on a variety of projects ranging from residential
buildings to commercial complexes. During my time there, I gained
hands-on experience in architectural design, drafting, and project
management, which significantly deepened my understanding of the
architectural process.
</p>
<p>
I also had the opportunity to collaborate with a team of experienced
architects and designers, learning from their expertise and
contributing to real-world projects. This experience was invaluable in
shaping my skills and preparing me for a career in architecture.
</p>
<p>
We worked on several notable projects, including A.R. Rahman's YM
Studios, a luxury spa in Tower Park, and various residential projects
in and around Chennai.
</p>
</>
),
logo: "BlueCube.png", logo: "BlueCube.png",
}, },
{ {
title: "OutFocus Magazine", title: "OutFocus Magazine",
slug: "outfocus",
location: "Ooty", location: "Ooty",
position: "Editor / Developer", position: "Editor / Developer",
year: "2014-17", year: "2014-17",
description: sampleText, tags: ["Design", "Programming", "Writing"],
description: (
<>
<p>
OutFocus Magazine was a diverse publication dedicated to exploring
arts, culture, lifestyle, and science. As the editor and developer, I
curated content, designed the layout, and managed the website.
OutFocus covered a wide range of topics, including visual arts, music,
literature, science, and emerging cultural trends.
</p>
<p>
This broad approach allowed us to engage a wide audience and provide a
platform for various voices and perspectives. Through this
exploration, we celebrated creativity, innovation, and intellectual
curiosity. I founded OutFocus with friends from The Feathers, and
together we created a unique and engaging publication that offered a
fresh perspective on contemporary culture and society. One of our
notable achievements was raising international awareness about the
Chennai floods through our social presence.
</p>
</>
),
logo: "OutFocus.png", logo: "OutFocus.png",
}, },
{ {
title: "Zoho", title: "Zoho",
slug: "zoho",
location: "Chennai", location: "Chennai",
position: "Technical Content Writer", position: "Technical Content Writer",
year: "2017", year: "2017",
description: sampleText, tags: ["Writing"],
description: (
<>
<p>
At Zoho, I served as a technical content writer, reporting directly to
the product manager. This role tasked me with creating documentation
and user guides for diverse software products. Under the product
manager's guidance, I ensured the content I produced aligned
seamlessly with each product's vision and objectives.
</p>
<p>
Working closely with the product manager streamlined collaboration and
decision-making processes. This direct line of communication
facilitated efficient feedback loops, allowing for the creation of
documentation that met both the product manager's standards and the
needs of end-users. Reporting solely to the product manager fostered a
strong sense of ownership and accountability, enabling me to deliver
high-quality technical content tailored to the specific requirements
of each software product.
</p>
</>
),
logo: "Zoho.png", logo: "Zoho.png",
}, },
{ {
title: "Manoj Exports", title: "Manoj Exports",
slug: "manoj",
location: "Chennai", location: "Chennai",
position: "Designer & Web Dev", position: "Designer & Web Dev",
year: "2017", year: "2017",
description: sampleText, tags: ["Design", "Programming"],
description: (
<>
<p>
Based in Chennai, Manoj Exports is an established company with a long
history. They've been exporting fruits and other goods to countries
around the world for many years. When they wanted to start selling
locally, they asked me to help them update their website and create a
new look for their brand. I worked closely with the company's owners
to understand what they wanted their business to be about.
</p>
<p>
Getting to know Manoj Exports well, I carefully designed a new website
and brand style that matched their values. Everything from the website
to the brand logo was made to show off the company's past while also
looking fresh and modern.
</p>
</>
),
logo: "ManojExports.png", logo: "ManojExports.png",
}, },
{ {
title: "Klenty", title: "Klenty",
slug: "klenty",
location: "Chennai", location: "Chennai",
position: "Full Stack Developer", position: "Full Stack Developer",
year: "2018", year: "2018",
description: sampleText, tags: ["Programming"],
description: (
<>
<p>
Klenty is a vibrant sales engagement platform crafted to streamline
communication processes for sales teams. It marked my debut in a
full-time programming role.
</p>
<p>
Joining during a period of rapid expansion, I encountered challenges
and opportunities for personal and professional growth. As a
full-stack developer, my responsibilities encompassed the development
and upkeep of the company's web applications. Working closely with the
product team, I spearheaded the integration of new features and
enhancements, ensuring a cohesive user experience across all
platforms.
</p>
<p>
Furthermore, I played a pivotal role in addressing challenges related
to scaling the system to accommodate the growing user base. I worked
to ensure the platform maintained optimal performance and reliability
even amidst increased usage and demand.
</p>
</>
),
logo: "Klenty.png", logo: "Klenty.png",
}, },
{ {
title: "Hugo's Way", title: "Hugo's Way",
slug: "hugosway",
location: "Remote (Dublin)", location: "Remote (Dublin)",
position: "Full Stack Developer", position: "Full Stack Developer",
year: "2018-19", year: "2018-19",
description: sampleText, tags: ["Programming"],
description: (
<>
<p>
At Hugo's Way, I played a key role in stabilising their currency
exchange platform, ensuring smooth and reliable operations.
Recognising the imperative for scalability and adaptability, I
spearheaded the transition from a monolithic architecture to a more
agile and modular service-oriented one to achieve flexibility to
deploy as whitelabelled services. This structural overhaul not only
bolstered performance but also streamlined development cycles and
maintenance.
</p>
<p>
Additionally, I established standards for comprehensive documentation,
ensuring clarity and consistency in our processes. Through effective
communication and mentorship, I fostered a culture of collaboration,
innovation, and continuous improvement within the team, driving our
collective efforts towards achieving greater success.
</p>
</>
),
logo: "Hugosway.png", logo: "Hugosway.png",
}, },
{ {
title: "Navana Tech", title: "Navana Tech",
slug: "navana",
location: "Remote (Mumbai)", location: "Remote (Mumbai)",
position: "Lead Web Dev & Architect", position: "Lead Web Dev & Architect",
year: "2021-22", year: "2021-22",
description: sampleText, tags: ["Programming", "Design"],
description: (
<>
<p>
During my time leading a team at Navana, I directed important projects
that shaped our path. Initially, I worked on building Zabaan, a
single-line import to make localisation easier in mobile apps. When
the company pivoted to sayl.ai, a cutting-edge WhatsApp marketing
platform for direct-to-consumer use, I played a big role in
architecting and leading the team in building this innovative product.
</p>
<p>
As team lead, I focused on building a strong team spirit. I encouraged
everyone to share ideas and work together. This made us more
productive and committed to our goals. I also made sure our processes
were well-documented, which helped us work smoothly and welcome new
team members easily. We organised weekly events to foster team bonding
and continuous learning, complemented by insightful blog posts
detailing our development journey.
</p>
</>
),
logo: "NavanaTech.png", logo: "NavanaTech.png",
}, },
{ {
title: "Feathers Studio", title: "Feathers Studio",
slug: "feathers-studio",
location: "Chennai", location: "Chennai",
position: "Chief Maker", position: "Chief Maker",
year: "2019-present", year: "2019-present",
description: sampleText, tags: ["Programming", "Design", "Writing"],
description: (
<>
<p>
At Feathers Studio, our transition from a creative collective to a
formally registered company has provided us with a platform for
innovation and experimentation. This shift allows us to explore
open-source initiatives and pursue ambitious moonshot ideas that
challenge conventional norms.
</p>
<p>
In addition to our exploration of cutting-edge concepts, we engage in
compelling projects aimed at redefining industry standards and
addressing critical user experience (UX) challenges. By pushing the
boundaries of creativity and technology, we continuously strive to
make meaningful contributions to our field while embracing a culture
of collaboration and innovation.
</p>
</>
),
logo: "FeathersStudio.png", logo: "FeathersStudio.png",
}, },
].reverse(); ].reverse();

57
src/util/useSearchParams.ts

@ -0,0 +1,57 @@
import { useState } from "react";
import useLocation from "wouter/use-location";
export type HookSet<T> = {
set: Set<T>;
size: number;
add: (value: T) => void;
remove: (value: T) => void;
clear: () => void;
has: (value: T) => boolean;
};
const flatten = (set: Set<string>, key: string) =>
Array.from(set)
.map(param => `${key}=${encodeURIComponent(param)}`)
.join("&");
export function useSearchParams(key: string): HookSet<string> {
const [, navigate] = useLocation();
const [params, setParams] = useState(
() => new Set(new URLSearchParams(window.location.search).getAll(key)),
);
const add = (value: string) => {
setParams(prevSet => {
const newSet = new Set(prevSet);
newSet.add(value);
navigate("?" + flatten(newSet, key), { replace: true });
return newSet;
});
};
const remove = (value: string) => {
setParams(prevSet => {
const newSet = new Set(prevSet);
newSet.delete(value);
navigate("?" + flatten(newSet, key), { replace: true });
return newSet;
});
};
const clear = () => {
setParams(new Set());
navigate("?", { replace: true });
};
return {
set: params,
size: params.size,
add,
remove,
clear,
has: (value: string) => params.has(value),
};
}
export default useSearchParams;
Loading…
Cancel
Save