wip: theme

api-as-package
pegasust 2022-11-20 04:37:35 +00:00
parent df126da2a6
commit 70f7b14a67
5 changed files with 322 additions and 275 deletions

View File

@ -1,6 +1,6 @@
import { ReactNode } from "react" import { ReactNode } from "react"
export const Section: React.FC<{children?: ReactNode}> = ({children}) => <section className="text-gray-700">{children}</section> export const Section: React.FC<{children?: ReactNode}> = ({children}) => <section className="text-gray-700 max-w-4xl">{children}</section>
export const UL: React.FC<{ children?: ReactNode }> = ({ children }) => <ul className="list-disc list-inside">{children}</ul> export const UL: React.FC<{ children?: ReactNode }> = ({ children }) => <ul className="list-disc list-inside">{children}</ul>
export const SectionHeader: React.FC<{ text?: string, children?: ReactNode }> = ({ text, children }) => <div> export const SectionHeader: React.FC<{ text?: string, children?: ReactNode }> = ({ text, children }) => <div>

View File

@ -1,15 +0,0 @@
import { Head } from "next/document";
import { Main } from "next/document";
import { NextScript } from "next/document";
import { Html } from "next/document";
import React from "react";
const document = () => <Html>
<Head />
<body className="scrollbar-track-gray-50 scrollbar-thumb-amber-400">
<Main />
<NextScript />
</body>
</Html>
export default document;

View File

View File

View File

@ -1,13 +1,13 @@
import { type NextPage } from "next"; import { type NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import React from "react"; import React, { useContext, useEffect, useState } from "react";
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority"; import type { VariantProps } from "class-variance-authority";
import Link from "next/link"; import Link from "next/link";
import { import {
FaGithub, FaGithub,
FaLinkedin FaLinkedin
} from 'react-icons/fa'; } from 'react-icons/fa';
import { ReactNode } from "react"; import { ReactNode } from "react";
import { useReducerMonad } from "@utils/stdx" import { useReducerMonad } from "@utils/stdx"
@ -21,338 +21,400 @@ import assert from "assert";
import { Section, SectionHeader, UL } from "@components/index"; import { Section, SectionHeader, UL } from "@components/index";
const circle = cva("absolute rounded-full border -z-10", { const circle = cva("absolute rounded-full border -z-10", {
variants: { variants: {
size: { size: {
0: "h-[200px] w-[200px]", 0: "h-[200px] w-[200px]",
1: "h-[350px] w-[350px]", 1: "h-[350px] w-[350px]",
2: "h-[500px] w-[500px]", 2: "h-[500px] w-[500px]",
3: "h-[650px] w-[650px]", 3: "h-[650px] w-[650px]",
4: "h-[800px] w-[800px]", 4: "h-[800px] w-[800px]",
},
animate: {
none: "",
pulse: "animate-pulse",
ping: "animate-ping",
},
theme: {
light: "",
dark: "",
},
color: {
back: "",
fore: "",
}
}, },
animate: { compoundVariants: [
none: "", {
pulse: "animate-pulse", theme: "light",
ping: "animate-ping", color: "back",
}, className: "border-pink-50"
color: { },
back: "border-pink-50", {
fore: "border-amber-300", theme: "light",
color: "fore",
className: "border-amber-300"
},
{
theme: "dark",
color: "back",
className: "border-violet-300"
},
{
theme: "dark",
color: "fore",
className: "border-amber-400"
},
],
defaultVariants: {
size: 0,
animate: "none",
color: "back",
theme: "light"
} }
},
defaultVariants: {
size: 0,
animate: "none",
color: "back"
}
}); });
const Circle: React.FC<VariantProps<typeof circle>> = const Circle: React.FC<VariantProps<typeof circle>> =
({ size, animate, color }) => <div className={circle({ ({ size, animate, color }) => <div className={circle({
size, animate, color size, animate, color
})} /> })} />
const Shapes = () => <div className="absolute flex items-center justify-center"> const Shapes = () => <div className="absolute flex items-center justify-center">
<Circle size={0} animate="ping" /> <Circle size={0} animate="ping" />
<Circle size={1} /> <Circle size={1} />
<Circle size={2} /> <Circle size={2} />
<Circle size={3} animate="pulse" color="fore" /> <Circle size={3} animate="pulse" color="fore" />
<Circle size={4} /> <Circle size={4} />
</div> </div>
const Article: React.FC<{ children?: ReactNode }> = ({ children }) => <article const Article: React.FC<{ children?: ReactNode }> = ({ children }) => <article
className="relative w-screen flex flex-col justify-center items-center overflow-hidden text-center min-h-screen gap-2"> className="relative w-screen flex flex-col justify-center items-center overflow-hidden text-center min-h-screen gap-2">
{children} {children}
</article> </article>
const InpageSection: React.FC<{ href: string, children?: ReactNode }> = ({ const InpageSection: React.FC<{ href: string, children?: ReactNode }> = ({
href, href,
children children
}) => <Link href={href} className="p-2 text-lg text-gray-500/50 rounded-3xl }) => <Link href={href} className="p-2 text-lg text-gray-500/50 rounded-3xl
border border-amber-400/50 hover:text-gray-500 hover:border-amber-400 transition-colors border border-amber-400/50 hover:text-gray-500 hover:border-amber-400 transition-colors
hover:animate-pulse"> hover:animate-pulse">
{children} {children}
</Link> </Link>
const Brief = () => <Article> const Brief = () => <Article>
{/*Avatar*/} {/*Avatar*/}
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<Shapes /> <Shapes />
<div className="rounded-full w-[200px] h-[200px] overflow-hidden flex justify-center"> <div className="rounded-full w-[200px] h-[200px] overflow-hidden flex justify-center">
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<Image <Image
src="/assets/avatarRL.jpeg" src="/assets/avatarRL.jpeg"
width={512} width={512}
height={512} height={512}
alt="Hung's avatar" /> alt="Hung's avatar" />
</div> </div>
</div>
</div>
<h1 className="text-gray-700 text-4xl semi-bold z-10">
Hung Tran
</h1>
<h1 className="text-gray-400 text-2xl tracking-widest z-10 uppercase">
Software Engineer
</h1>
<div className="flex flex-row gap-4">
<InpageSection href="#about">About</InpageSection>
<InpageSection href="#experience">Experience</InpageSection>
<InpageSection href="#blog">Blog</InpageSection>
<InpageSection href="#projects">Projects</InpageSection>
</div> </div>
</div>
<h1 className="text-gray-700 text-4xl semi-bold z-10">
Hung Tran
</h1>
<h1 className="text-gray-400 text-2xl tracking-widest z-10 uppercase">
Software Engineer
</h1>
<div className="flex flex-row gap-4">
<InpageSection href="#about">About</InpageSection>
<InpageSection href="#experience">Experience</InpageSection>
<InpageSection href="#blog">Blog</InpageSection>
<InpageSection href="#projects">Projects</InpageSection>
</div>
</Article> </Article>
const images = [ const images = [
{ {
src: SFChinatown, src: SFChinatown,
alt: "chinatown @ San Francisco" alt: "chinatown @ San Francisco"
}, },
{ {
src: SFChurch, src: SFChurch,
alt: "church @ San Francisco" alt: "church @ San Francisco"
}, },
{ {
src: Lizzie, src: Lizzie,
alt: "My cat Lizzie", alt: "My cat Lizzie",
}, },
{ {
src: SantaMonicaHigh, src: SantaMonicaHigh,
alt: "Santa Monica in high ground" alt: "Santa Monica in high ground"
}, },
{ {
src: SantaMonicaStairs, src: SantaMonicaStairs,
alt: "Santa Monica taken near stairs" alt: "Santa Monica taken near stairs"
} }
]; ];
type Image = typeof images[0]; type Image = typeof images[0];
function useImageDisplay(images: Image[]) { function useImageDisplay(images: Image[]) {
const [imageRepo, setImageRepo] = React.useState(images); const [imageRepo, setImageRepo] = React.useState(images);
const init = images.length == 0 ? undefined : 0; const init = images.length == 0 ? undefined : 0;
const [imgIndex, imgDispatch] = useReducerMonad((idx, action: { const [imgIndex, imgDispatch] = useReducerMonad((idx, action: {
type: "next" | "prev" type: "next" | "prev"
} | { } | {
type: "set", type: "set",
index: number index: number
}) => { }) => {
if (images.length == 0) { return undefined; } if (images.length == 0) { return undefined; }
let retval = idx; let retval = idx;
assert(retval != undefined); assert(retval != undefined);
switch (action.type) { switch (action.type) {
case "next": case "next":
retval += 1; retval += 1;
break; break;
case "prev": case "prev":
retval -= 1; retval -= 1;
break; break;
case "set": case "set":
return action.index; return action.index;
}
retval = (images.length + retval) % images.length;
return retval;
}, init);
const image = imgIndex !== undefined ? imageRepo[imgIndex] : undefined;
return {
next: () => imgDispatch({ type: "next" }),
prev: () => imgDispatch({ type: "prev" }),
setIdx: (idx: number) => imgDispatch({ type: "set", index: idx }),
imageRepo,
setImageRepo,
imgIndex,
image,
} }
retval = (images.length + retval) % images.length;
return retval;
}, init);
const image = imgIndex !== undefined ? imageRepo[imgIndex] : undefined;
return {
next: () => imgDispatch({ type: "next" }),
prev: () => imgDispatch({ type: "prev" }),
setIdx: (idx: number) => imgDispatch({ type: "set", index: idx }),
imageRepo,
setImageRepo,
imgIndex,
image,
}
} }
type ImageDisplayHook = ReturnType<typeof useImageDisplay>; type ImageDisplayHook = ReturnType<typeof useImageDisplay>;
const imageButton = cva("w-8 h-8 rounded-full flex justify-center items-center transition-colors font-semibold", { const imageButton = cva("w-8 h-8 rounded-full flex justify-center items-center transition-colors font-semibold", {
variants: { variants: {
theme: { theme: {
light: "bg-amber-200/30 group-hover:bg-amber-200/60 text-gray-700" light: "bg-amber-200/30 group-hover:bg-amber-200/60 text-gray-700"
}
},
defaultVariants: {
theme: "light"
} }
},
defaultVariants: {
theme: "light"
}
}); });
const imageIndex = cva("rounded-full hover:cursor-pointer transition-all", { const imageIndex = cva("rounded-full hover:cursor-pointer transition-all", {
variants: { variants: {
theme: { theme: {
light: "", light: "",
},
active: {
isActive: "w-3 h-3",
isInactive: "w-2 h-2",
}
}, },
active: { compoundVariants: [
isActive: "w-3 h-3", {
isInactive: "w-2 h-2", theme: "light",
active: "isActive",
className: "bg-pink-200/70"
},
{
theme: "light",
active: "isInactive",
className: "bg-pink-200/40"
}
],
defaultVariants: {
theme: "light",
} }
},
compoundVariants: [
{
theme: "light",
active: "isActive",
className: "bg-pink-200/70"
},
{
theme: "light",
active: "isInactive",
className: "bg-pink-200/40"
}
],
defaultVariants: {
theme: "light",
}
}) })
const ImageDisplay: React.FC<{ imageDisplayHook: ImageDisplayHook }> = ({ imageDisplayHook: { const ImageDisplay: React.FC<{ imageDisplayHook: ImageDisplayHook }> = ({ imageDisplayHook: {
next, next,
prev, prev,
imageRepo, imageRepo,
// setImageRepo, // setImageRepo,
imgIndex, imgIndex,
image image
} }) => <div className="relative bg-gray-100/30 rounded-md overflow-hidden"> } }) => <div className="relative bg-gray-100/30 rounded-md overflow-hidden">
<button className="group left-0 absolute w-5/12 h-full flex flex-row justify-start items-center" onClick={prev}> <button className="group left-0 absolute w-5/12 h-full flex flex-row justify-start items-center" onClick={prev}>
<span className={imageButton()}>{"<"}</span> <span className={imageButton()}>{"<"}</span>
</button> </button>
<button className="group right-0 absolute w-5/12 h-full flex flex-row-reverse justify-start items-center" onClick={next}> <button className="group right-0 absolute w-5/12 h-full flex flex-row-reverse justify-start items-center" onClick={next}>
<span className={imageButton()}>{">"}</span> <span className={imageButton()}>{">"}</span>
</button> </button>
<div className="flex justify-center items-center w-2/6 absolute bottom-0 left-[33.3333%] h-14"> <div className="flex justify-center items-center w-2/6 absolute bottom-0 left-[33.3333%] h-14">
<div className="flex flex-row justify-between items-center gap-2"> <div className="flex flex-row justify-between items-center gap-2">
{Array.from(Array(imageRepo.length).keys()).map(i => <div className={imageIndex({ {Array.from(Array(imageRepo.length).keys()).map(i => <div className={imageIndex({
active: (i == imgIndex ? "isActive" : "isInactive"), active: (i == imgIndex ? "isActive" : "isInactive"),
theme: "light" theme: "light"
})} })}
key={`img-index-${i}`} />)} key={`img-index-${i}`} />)}
</div> </div>
</div>
{image && <Image placeholder="blur" src={image.src} alt={image.alt} className="w-96 h-96 object-contain rounded-md overflow-hidden transition-all" />}
</div> </div>
{image && <Image placeholder="blur" src={image.src} alt={image.alt} className="w-96 h-96 object-contain rounded-md overflow-hidden transition-all" />}
</div>
const MyLink: React.FC<{ href: string, children?: ReactNode }> = ({ href, children }) => <Link const MyLink: React.FC<{ href: string, children?: ReactNode }> = ({ href, children }) => <Link
className="text-amber-400/70 visited:text-purple-400/70 hover:text-amber-400 visited:hover:text-purple-400" href={href}> className="text-amber-400/70 visited:text-purple-400/70 hover:text-amber-400 visited:hover:text-purple-400" href={href}>
{children} {children}
</Link> </Link>
const About = () => { const About = () => {
const imgDisplay = useImageDisplay(images); const imgDisplay = useImageDisplay(images);
return <Article> return <Article>
<SectionHeader>About</SectionHeader> <SectionHeader>About</SectionHeader>
<div className="flex flex-row gap-4"> <div className="flex flex-row gap-4">
<ImageDisplay imageDisplayHook={imgDisplay} /> <ImageDisplay imageDisplayHook={imgDisplay} />
<div className="text-left"> <div className="text-left">
<Section> <Section>
<span> <span>
Aspiring software engineer with intense drive and curiosity in software development. Aspiring software engineer with intense drive and curiosity in software development.
</span> </span>
</Section> </Section>
<Section className="max-w-4xl"> <Section>
My computer infrastructure consists of: My computer infrastructure consists of:
<UL> <UL>
<li>24 home CPU cores</li> <li>24 home CPU cores</li>
<li>60 GB of RAM (4 GB SDDR3 and 56 GB DDR4)</li> <li>60 GB of RAM (4 GB SDDR3 and 56 GB DDR4)</li>
<li>2.5 TB of SSD + HDD</li> <li>2.5 TB of SSD + HDD</li>
<li>A mix of NixOS, Ubuntu, and Windows</li> <li>A mix of NixOS, Ubuntu, and Windows</li>
</UL> </UL>
</Section> </Section>
<Section> <Section>
<span> <span>
In free time, you would find me: In free time, you would find me:
</span> </span>
<UL> <UL>
<li>Learning about new technology in a week</li> <li>Learning about new technology in a week</li>
<li>Performing microbenchmarks on competing implementations and technologies</li> <li>Performing microbenchmarks on competing implementations and technologies</li>
<li>Optimizing my workspace through <MyLink href="https://git.pegasust.com/pegasust/dotfiles.git">dotfiles</MyLink></li> <li>Optimizing my workspace through <MyLink href="https://git.pegasust.com/pegasust/dotfiles.git">dotfiles</MyLink></li>
<li>Contributing to open-source projects</li> <li>Contributing to open-source projects</li>
<li>Watching edutainment videos and podcasts about software</li> {/*TODO: add link to edutainment note-taking platform here*/} <li>Watching edutainment videos and podcasts about software</li> {/*TODO: add link to edutainment note-taking platform here*/}
<li><MyLink href="https://soundcloud.com/h-ng-tr-n-186908751">Producing music</MyLink></li> <li><MyLink href="https://soundcloud.com/h-ng-tr-n-186908751">Producing music</MyLink></li>
</UL> </UL>
</Section> </Section>
</div> </div>
</div> </div>
</Article> </Article>
} }
const Experience = () => <Article> const Experience = () => <Article>
<SectionHeader>Experience</SectionHeader> <SectionHeader>Experience</SectionHeader>
</Article> </Article>
const Blog = () => <Article> const Blog = () => <Article>
<SectionHeader>Blogs</SectionHeader> <SectionHeader>Blogs</SectionHeader>
Currently have a Zettelkasten, but nothing is published yet. Currently have a Zettelkasten, but nothing is published yet.
</Article> </Article>
const Projects = () => <Article > const Projects = () => <Article >
<SectionHeader>Projects</SectionHeader> <SectionHeader>Projects</SectionHeader>
</Article> </Article>
const NavbarItem: React.FC<{ const NavbarItem: React.FC<{
text: string; text: string;
link: string; link: string;
children: React.ReactNode children: React.ReactNode
}> = ({ text, link, children }) => <Link className="group" href={link}> }> = ({ text, link, children }) => <Link className="group" href={link}>
<button className="flex flex-col items-center justify-center flex-flex-col p-2 group-hover:cursor-pointer group"> <button className="flex flex-col items-center justify-center flex-flex-col p-2 group-hover:cursor-pointer group">
{children} {children}
{/*<span className="group-hover:visible invisible">{text}</span>*/} {/*<span className="group-hover:visible invisible">{text}</span>*/}
</button> </button>
</Link> </Link>
const iconCva = cva("transition-colors", { const iconCva = cva("transition-colors", {
variants: { variants: {
theme: { theme: {
light: "group-hover:fill-gray-700 fill-gray-400" light: "group-hover:fill-gray-700 fill-gray-400"
}
},
defaultVariants: {
theme: "light"
} }
},
defaultVariants: {
theme: "light"
}
}); });
const Navbar = () => <div className="flex justify-center items-center w-full z-50 sticky top-0 left-0 backdrop-blur-md h-8"> const Navbar = () => <div className="flex justify-center items-center w-full z-50 sticky top-0 left-0 backdrop-blur-md h-8">
<div className="w-11/12 max-w-screen-lg flex items-center gap-4"> <div className="w-11/12 max-w-screen-lg flex items-center gap-4">
<div className="flex flex-row justify-center items-center gap-1"> <div className="flex flex-row justify-center items-center gap-1">
<NavbarItem text="GitHub" link="https://github.com/pegasust"><FaGithub className={iconCva()} /></NavbarItem> <NavbarItem text="GitHub" link="https://github.com/pegasust"><FaGithub className={iconCva()} /></NavbarItem>
<NavbarItem text="LinkedIn" link="https://linkedin.com/in/hung-tran-1963bb205/"><FaLinkedin className={iconCva()} /></NavbarItem> <NavbarItem text="LinkedIn" link="https://linkedin.com/in/hung-tran-1963bb205/"><FaLinkedin className={iconCva()} /></NavbarItem>
</div>
<Link href="#brief">
<span className="transition-colors hover:text-amber-400 text-amber-400/50 animate-pulse">Hung Tran</span>
</Link>
</div> </div>
<Link href="#brief">
<span className="transition-colors hover:text-amber-400 text-amber-400/50 animate-pulse">Hung Tran</span>
</Link>
</div>
</div> </div>
type Theme = "dark" | "light";
const ThemeContext = React.createContext<{
theme: Theme,
systemTheme: Theme,
setThemePreference(theme: Theme): void,
} | undefined>(undefined);
const ThemeProvider: React.FC<{ children: React.ReactNode, prefer?: "light" | "dark" }> = ({ children, prefer }) => {
const pref = prefer ?? "light";
const [systemTheme, setSystemTheme] = useState<Theme>(pref);
const [themePref, setThemePref] = useState<Theme | undefined>(undefined);
// add a listener to system preference on which theme
// TODO: is there another way that remove event listener faster?
useEffect(() => {
console.log("useEffect called");
type EventType = { matches: boolean };
const onThemeChange = (event: EventType) => {
const newColorScheme = event.matches ? "dark" : "light";
setSystemTheme(newColorScheme);
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', onThemeChange);
return () => {
console.log("useEffect unmount");
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener("change", onThemeChange)
}
}, []);
return <ThemeContext.Provider value={{
theme: themePref ?? systemTheme,
systemTheme: systemTheme,
setThemePreference: setThemePref,
}}>{children}</ThemeContext.Provider>
}
const useTheme = () => useContext(ThemeContext);
const Home: NextPage = () => { const Home: NextPage = () => {
return ( return (
<main className="overflow-x-hidden z-0 flex flex-col overflow-y-scroll h-screen <ThemeProvider>
items-center bg-pink-50/20 min-h-screen antialiased snap-y snap-proximity <main className="overflow-x-hidden z-0 flex flex-col overflow-y-scroll h-screen
scrollbar-track-gray-200 scrollbar-thumb-amber-300/50 scrollbar-thin"> {/*vimium users cry if snap-mandatory*/} items-center bg-pink-50/20 min-h-screen antialiased snap-y snap-proximity
<Head> scrollbar-track-gray-200 scrollbar-thumb-amber-300/50 scrollbar-thin"> {/*vimium users cry if snap-mandatory*/}
<title>Hung Tran</title> <Head>
<meta name="description" content="Personal website about Hung Tran and my homelab Felia" /> <title>Hung Tran</title>
<link rel="icon" href="/favicon.ico" /> <meta name="description" content="Personal website about Hung Tran and my homelab Felia" />
</Head> <link rel="icon" href="/favicon.ico" />
<Navbar /> </Head>
<section id="brief" className="snap-start"> <Navbar />
<Brief /> <section id="brief" className="snap-start">
</section> <Brief />
<section id="about" className="snap-start"> </section>
<About /> <section id="about" className="snap-start">
</section> <About />
<section id="experience" className="snap-start"> </section>
<Experience /> <section id="experience" className="snap-start">
</section> <Experience />
<section id="blog" className="snap-start"> </section>
<Blog /> <section id="blog" className="snap-start">
</section> <Blog />
<section id="projects" className="snap-start"> </section>
<Projects /> <section id="projects" className="snap-start">
</section> <Projects />
</main> </section>
); </main>
</ThemeProvider>
);
}; };
export default Home; export default Home;