|
|
@ -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, { useContext, useEffect, useState } from "react";
|
|
|
|
import React 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"
|
|
|
@ -18,413 +18,347 @@ import Lizzie from "@public/assets/lizzie.jpg";
|
|
|
|
import SantaMonicaStairs from "@public/assets/santa-monica-stairs.jpg";
|
|
|
|
import SantaMonicaStairs from "@public/assets/santa-monica-stairs.jpg";
|
|
|
|
import SantaMonicaHigh from "@public/assets/santa-monica-high.jpg";
|
|
|
|
import SantaMonicaHigh from "@public/assets/santa-monica-high.jpg";
|
|
|
|
import assert from "assert";
|
|
|
|
import assert from "assert";
|
|
|
|
import { Section, SectionHeader, UL } from "@components/index";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 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: "",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
compoundVariants: [
|
|
|
|
animate: {
|
|
|
|
{
|
|
|
|
none: "",
|
|
|
|
theme: "light",
|
|
|
|
pulse: "animate-pulse",
|
|
|
|
color: "back",
|
|
|
|
ping: "animate-ping",
|
|
|
|
className: "border-pink-50"
|
|
|
|
},
|
|
|
|
},
|
|
|
|
color: {
|
|
|
|
{
|
|
|
|
back: "border-pink-50",
|
|
|
|
theme: "light",
|
|
|
|
fore: "border-amber-300",
|
|
|
|
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 SectionHeader: React.FC<{ text?: string, children?: ReactNode }> = ({ text, children }) => <div>
|
|
|
|
|
|
|
|
<h1 className="text-xl text-gray-600 tracking-widest">
|
|
|
|
|
|
|
|
{text}
|
|
|
|
|
|
|
|
{children}
|
|
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
<div className="mt-8" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
compoundVariants: [
|
|
|
|
active: {
|
|
|
|
{
|
|
|
|
isActive: "w-3 h-3",
|
|
|
|
theme: "light",
|
|
|
|
isInactive: "w-2 h-2",
|
|
|
|
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">
|
|
|
|
const {theme} = useTheme()!;
|
|
|
|
<button className="group left-0 absolute w-5/12 h-full flex flex-row justify-start items-center" onClick={prev}>
|
|
|
|
return <div className="relative h-fit w-fit bg-gray-100/30 rounded-md">
|
|
|
|
<span className={imageButton()}>{"<"}</span>
|
|
|
|
<button className="group left-0 absolute w-5/12 h-full flex flex-row justify-start items-center" onClick={prev}>
|
|
|
|
</button>
|
|
|
|
<span className={imageButton()}>{"<"}</span>
|
|
|
|
<button className="group right-0 absolute w-5/12 h-full flex flex-row-reverse justify-start items-center" onClick={next}>
|
|
|
|
</button>
|
|
|
|
<span className={imageButton()}>{">"}</span>
|
|
|
|
<button className="group right-0 absolute w-5/12 h-full flex flex-row-reverse justify-start items-center" onClick={next}>
|
|
|
|
</button>
|
|
|
|
<span className={imageButton()}>{">"}</span>
|
|
|
|
<div className="flex justify-center items-center w-2/6 absolute bottom-0 left-[33.3333%] h-14">
|
|
|
|
</button>
|
|
|
|
<div className="flex flex-row justify-between items-center gap-2">
|
|
|
|
<div className="flex justify-center items-center w-2/6 absolute bottom-0 left-[33.3333%] h-14">
|
|
|
|
{Array.from(Array(imageRepo.length).keys()).map(i => <div className={imageIndex({
|
|
|
|
<div className="flex flex-row justify-between items-center gap-2">
|
|
|
|
active: (i == imgIndex ? "isActive" : "isInactive"),
|
|
|
|
{Array.from(Array(imageRepo.length).keys()).map(i => <div className={imageIndex({
|
|
|
|
theme: "light"
|
|
|
|
active: (i == imgIndex ? "isActive" : "isInactive"),
|
|
|
|
})}
|
|
|
|
theme: theme
|
|
|
|
key={`img-index-${i}`} />)}
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
key={`img-index-${i}`} />)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{image && <Image placeholder="blur" src={image.src} alt={image.alt}
|
|
|
|
|
|
|
|
className="w-96 h-96 object-contain rounded-md" />}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
{image && <Image 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-300/70 hover:text-amber-400 visited:hover:text-purple-300" 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 w-11/12 justify-evenly max-w-screen-lg gap-8">
|
|
|
|
<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>
|
|
|
|
<section className="max-w-4xl">
|
|
|
|
My computer infrastructure consists of:
|
|
|
|
My computer infrastructure consists of:
|
|
|
|
<UL>
|
|
|
|
<ul className="list-disc list-inside">
|
|
|
|
<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 className="list-disc list-inside">
|
|
|
|
<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 group-hover:animate-bounce", {
|
|
|
|
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 = () => {
|
|
|
|
const Navbar = () => <div className="flex justify-center items-center w-full z-50 sticky top-0 left-0 backdrop-blur-md h-8">
|
|
|
|
const _theme = useTheme();
|
|
|
|
<div className="w-11/12 max-w-screen-lg flex items-center gap-4">
|
|
|
|
if (!_theme) { throw new Error("no theme provider"); }
|
|
|
|
<div className="flex flex-row justify-center items-center gap-1">
|
|
|
|
const { theme, setThemePreference } = _theme;
|
|
|
|
<NavbarItem text="GitHub" link="https://github.com/pegasust"><FaGithub className={iconCva()} /></NavbarItem>
|
|
|
|
return <div className="flex justify-center items-center w-full z-50 sticky top-0 left-0 backdrop-blur-md h-8">
|
|
|
|
<NavbarItem text="LinkedIn" link="https://linkedin.com/in/hung-tran-1963bb205/"><FaLinkedin className={iconCva()} /></NavbarItem>
|
|
|
|
<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">
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Link href="#brief">
|
|
|
|
|
|
|
|
<span className="transition-colors hover:text-amber-400 text-amber-400/50 hover:animate-pulse">Hung Tran</span>
|
|
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Home: NextPage = () => {
|
|
|
|
const Home: NextPage = () => {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<ThemeProvider>
|
|
|
|
<main className="overflow-x-hidden z-0 flex flex-col overflow-y-scroll h-screen
|
|
|
|
<main className="overflow-x-hidden z-0 flex flex-col overflow-y-scroll h-screen
|
|
|
|
items-center bg-pink-50/20 min-h-screen antialiased snap-y snap-proximity
|
|
|
|
items-center bg-pink-50/20 min-h-screen antialiased snap-y snap-proximity
|
|
|
|
scrollbar-track-gray-200 scrollbar-thumb-amber-300/50 scrollbar-thin"> {/*vimium users cry if snap-mandatory*/}
|
|
|
|
scrollbar-track-gray-200 scrollbar-thumb-amber-300/50 scrollbar-thin"> {/*vimium users cry if snap-mandatory*/}
|
|
|
|
<Head>
|
|
|
|
<Head>
|
|
|
|
<title>Hung Tran</title>
|
|
|
|
<title>Hung Tran</title>
|
|
|
|
<meta name="description" content="Personal website about Hung Tran and my homelab Felia" />
|
|
|
|
<meta name="description" content="Personal website about Hung Tran and my homelab Felia" />
|
|
|
|
<link rel="icon" href="/favicon.ico" />
|
|
|
|
<link rel="icon" href="/favicon.ico" />
|
|
|
|
</Head>
|
|
|
|
</Head>
|
|
|
|
<Navbar />
|
|
|
|
<Navbar />
|
|
|
|
<section id="brief" className="snap-start">
|
|
|
|
<section id="brief" className="snap-start">
|
|
|
|
<Brief />
|
|
|
|
<Brief />
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<section id="about" className="snap-start">
|
|
|
|
<section id="about" className="snap-start">
|
|
|
|
<About />
|
|
|
|
<About />
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<section id="experience" className="snap-start">
|
|
|
|
<section id="experience" className="snap-start">
|
|
|
|
<Experience />
|
|
|
|
<Experience />
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<section id="blog" className="snap-start">
|
|
|
|
<section id="blog" className="snap-start">
|
|
|
|
<Blog />
|
|
|
|
<Blog />
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<section id="projects" className="snap-start">
|
|
|
|
<section id="projects" className="snap-start">
|
|
|
|
<Projects />
|
|
|
|
<Projects />
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
</main>
|
|
|
|
</main>
|
|
|
|
);
|
|
|
|
</ThemeProvider>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default Home;
|
|
|
|
export default Home;
|
|
|
|