Compare commits
No commits in common. "064a9355e624b87c356545363e8c937398b7c272" and "2869e66a0261d96c91e072eb4bbb281af1669c13" have entirely different histories.
064a9355e6
...
2869e66a02
11
README.md
11
README.md
|
@ -5,6 +5,7 @@ Scaffolded by `pnpm create turbo@latest`
|
||||||
This project manages my homelab infrastructure under a monorepo to keep things
|
This project manages my homelab infrastructure under a monorepo to keep things
|
||||||
centralized and easily sharable across different modules
|
centralized and easily sharable across different modules
|
||||||
|
|
||||||
|
|
||||||
# Turbo-repo details
|
# Turbo-repo details
|
||||||
|
|
||||||
## What's inside?
|
## What's inside?
|
||||||
|
@ -13,11 +14,11 @@ This turborepo uses [pnpm](https://pnpm.io) as a package manager. It includes th
|
||||||
|
|
||||||
### Apps and Packages
|
### Apps and Packages
|
||||||
|
|
||||||
- `apps/docs`: a [Next.js](https://nextjs.org/) app
|
- `docs`: a [Next.js](https://nextjs.org/) app
|
||||||
- `apps/web`: another [Next.js](https://nextjs.org/) app
|
- `web`: another [Next.js](https://nextjs.org/) app
|
||||||
- `packages/ui`: a stub React component library shared by both `web` and `docs` applications
|
- `ui`: a stub React component library shared by both `web` and `docs` applications
|
||||||
- `packages/eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
|
- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
|
||||||
- `packages/tsconfig`: `tsconfig.json`s used throughout the monorepo
|
- `tsconfig`: `tsconfig.json`s used throughout the monorepo
|
||||||
|
|
||||||
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { ReactNode } from "react"
|
|
||||||
|
|
||||||
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 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>
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
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;
|
|
@ -1,7 +1,7 @@
|
||||||
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";
|
||||||
|
@ -18,42 +18,6 @@ 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: {
|
||||||
|
@ -69,42 +33,15 @@ const circle = cva("absolute rounded-full border -z-10", {
|
||||||
pulse: "animate-pulse",
|
pulse: "animate-pulse",
|
||||||
ping: "animate-ping",
|
ping: "animate-ping",
|
||||||
},
|
},
|
||||||
theme: {
|
|
||||||
light: "",
|
|
||||||
dark: "",
|
|
||||||
},
|
|
||||||
color: {
|
color: {
|
||||||
back: "",
|
back: "border-pink-50",
|
||||||
fore: "",
|
fore: "border-amber-300",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
|
||||||
{
|
|
||||||
theme: "light",
|
|
||||||
color: "back",
|
|
||||||
className: "border-pink-50"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: {
|
defaultVariants: {
|
||||||
size: 0,
|
size: 0,
|
||||||
animate: "none",
|
animate: "none",
|
||||||
color: "back",
|
color: "back"
|
||||||
theme: "light"
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -127,14 +64,14 @@ const Article: React.FC<{ children?: ReactNode }> = ({ children }) => <article
|
||||||
</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*/}
|
||||||
|
@ -164,6 +101,13 @@ const Brief = () => <Article>
|
||||||
</div>
|
</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 = [
|
||||||
{
|
{
|
||||||
|
@ -272,9 +216,7 @@ const ImageDisplay: React.FC<{ imageDisplayHook: ImageDisplayHook }> = ({ imageD
|
||||||
// setImageRepo,
|
// setImageRepo,
|
||||||
imgIndex,
|
imgIndex,
|
||||||
image
|
image
|
||||||
} }) => {
|
} }) => <div className="relative bg-gray-100/30 rounded-md overflow-hidden">
|
||||||
const {theme} = useTheme()!;
|
|
||||||
return <div className="relative h-fit w-fit bg-gray-100/30 rounded-md">
|
|
||||||
<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>
|
||||||
|
@ -285,18 +227,16 @@ const ImageDisplay: React.FC<{ imageDisplayHook: ImageDisplayHook }> = ({ imageD
|
||||||
<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: theme
|
theme: "light"
|
||||||
})}
|
})}
|
||||||
key={`img-index-${i}`} />)}
|
key={`img-index-${i}`} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{image && <Image placeholder="blur" src={image.src} alt={image.alt}
|
{image && <Image src={image.src} alt={image.alt} className="w-96 h-96 object-contain rounded-md overflow-hidden transition-all" />}
|
||||||
className="w-96 h-96 object-contain rounded-md" />}
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
@ -305,36 +245,36 @@ const About = () => {
|
||||||
|
|
||||||
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>
|
||||||
|
@ -367,7 +307,7 @@ const NavbarItem: React.FC<{
|
||||||
</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"
|
||||||
|
@ -378,26 +318,21 @@ const iconCva = cva("transition-colors group-hover:animate-bounce", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
if (!_theme) { throw new Error("no theme provider"); }
|
|
||||||
const { theme, setThemePreference } = _theme;
|
|
||||||
return <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>
|
</div>
|
||||||
<Link href="#brief">
|
<Link href="#brief">
|
||||||
<span className="transition-colors hover:text-amber-400 text-amber-400/50 hover:animate-pulse">Hung Tran</span>
|
<span className="transition-colors hover:text-amber-400 text-amber-400/50 animate-pulse">Hung Tran</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</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*/}
|
||||||
|
@ -423,7 +358,6 @@ const Home: NextPage = () => {
|
||||||
<Projects />
|
<Projects />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</ThemeProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,8 @@
|
||||||
"@server/*": ["src/server/*"],
|
"@server/*": ["src/server/*"],
|
||||||
"@db/*": ["src/server/db/*"],
|
"@db/*": ["src/server/db/*"],
|
||||||
"@trpc/*": ["src/server/trpc/*"],
|
"@trpc/*": ["src/server/trpc/*"],
|
||||||
"@router/*": ["src/server/trpc/router/*"],
|
"@router/*": ["src/server/trpc/router"],
|
||||||
"@public/*": ["public/*"],
|
"@public/*": ["public/*"]
|
||||||
"@components/*": ["src/components/*"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,19 @@
|
||||||
"extends": "./nextjs.json",
|
"extends": "./nextjs.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@env/*": ["src/env/*"],
|
||||||
|
"@pages/*": ["src/pages/*"],
|
||||||
|
"@styles/*": ["src/styles/*"],
|
||||||
|
"@utils/*": ["src/utils/*"],
|
||||||
|
"@server/*": ["src/server/*"],
|
||||||
|
"@db/*": ["src/server/db/*"],
|
||||||
|
"@trpc/*": ["src/server/trpc/*"],
|
||||||
|
"@router/*": ["src/server/trpc/router"],
|
||||||
|
"@public/*": ["public/*"]
|
||||||
|
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
|
|
Loading…
Reference in New Issue