···11+# bun-react-tailwind-shadcn-template
22+33+To install dependencies:
44+55+```bash
66+bun install
77+```
88+99+To start a development server:
1010+1111+```bash
1212+bun dev
1313+```
1414+1515+To run for production:
1616+1717+```bash
1818+bun start
1919+```
2020+2121+This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···11+type DescriptionPart = {
22+ text: string
33+ bold?: boolean
44+ url?: string
55+}
66+77+export const personalInfo = {
88+ name: {
99+ first: "at://nekomimi.pet",
1010+ last: "",
1111+ },
1212+ title: "Developer, cat",
1313+ description: [
1414+ { text: "A cat working on " },
1515+ { text: "useful", bold: true },
1616+ { text: ", " },
1717+ { text: "genuine", bold: true },
1818+ { text: " " },
1919+ { text: "decentralized", bold: true, url: "https://atproto.com" },
2020+ { text: " experiences. Not Web3. Also likes to " },
2121+ { text: "draw", bold: true, url: "https://bsky.app/profile/art.nekomimi.pet" },
2222+ { text: "." },
2323+ ],
2424+ availability: {
2525+ status: "Available for work",
2626+ location: "Richmond, VA, USA",
2727+ },
2828+ contact: {
2929+ email: "ana@nekomimi.pet",
3030+ },
3131+}
3232+3333+export const currentRole = {
3434+ title: "Freelance",
3535+ company: "maybe your company :)",
3636+ period: "2021 — Present",
3737+}
3838+3939+export const skills = ["React", "TypeScript", "Go", "Devops/Infra", "Atproto", "Cryptocurrencies"]
4040+4141+export const workExperience = [
4242+ {
4343+ year: "2020-2025",
4444+ role: "Fullstack Engineer, Infra/DevOps, Security Reviews",
4545+ company: "Freelance",
4646+ description: "Partook in various freelance work while studying for my bachelor's. Took a strong interest in the AT Protocol and started building libraries to support it.",
4747+ tech: ["React", "TypeScript", "Bun", "SQL"],
4848+ projects: [
4949+ {
5050+ title: "atproto-ui",
5151+ description: "A React component for rendering common UI elements across AT applications like Bluesky, Leaflet.pub, and more.",
5252+ tech: ["React", "TypeScript"],
5353+ links: {
5454+ live: "https://atproto-ui.netlify.app/",
5555+ github: "https://tangled.org/@nekomimi.pet/atproto-ui",
5656+ },
5757+ },
5858+ {
5959+ title: "wisp.place",
6060+ description: "A static site hoster built on the AT protocol. Users retain control over site data and content while Wisp acts as a CDN.",
6161+ tech: ["React", "TypeScript", "Bun", "ElysiaJS", "Hono", "Docker"],
6262+ links: {
6363+ live: "https://wisp.place",
6464+ github: "#",
6565+ },
6666+ },
6767+ {
6868+ title: "Confidential",
6969+ description: "NixOS consulting. Maintaining automated deployments, provisioning, and configuration management.",
7070+ tech: ["Nix", "Terraform"],
7171+ },
7272+ {
7373+ title: "Embedder",
7474+ description: "An unbloated media self-hostable service specialized in great looking embeds for services like Discord. Autocompresses videos using FFmpeg. Loved by many gamers and has over 2k installs.",
7575+ tech: ["HTMX", "Express", "TypeScript"],
7676+ links: {
7777+ github: "https://github.com/waveringana/embedder",
7878+ },
7979+ }
8080+ ],
8181+ },
8282+ {
8383+ year: "2016-2019",
8484+ role: "Software Engineer, DevOps",
8585+ company: "Horizen.io, later freelance",
8686+ description: "Helped launch Horizen, built various necessary blockchain infrastructure like explorers and pools. Managed CI/CD pipelines, infrastructure, and community support.",
8787+ tech: ["Node.js", "Jenkins", "C++"],
8888+ projects: [
8989+ {
9090+ title: "Z-NOMP",
9191+ description: "A port of NOMP for Equihash Coins, built using Node.js and Redis.",
9292+ tech: ["Node.js", "Redis"],
9393+ links: {
9494+ github: "https://github.com/z-classic/z-nomp",
9595+ },
9696+ },
9797+ {
9898+ title: "Equihash-Solomining",
9999+ description: "Pool software dedicated for Solomining, initially for equihash but for private clients, worked to adapt to PoW of their needs.",
100100+ tech: ["Node.js", "Redis", "d3.js"],
101101+ links: {
102102+ github: "https://github.com/waveringana/equihash-solomining",
103103+ },
104104+ }
105105+ ]
106106+ },
107107+]
108108+109109+export const socialLinks = [
110110+ { name: "GitHub", handle: "@waveringana", url: "https://github.com/waveringana" },
111111+ { name: "Bluesky", handle: "@nekomimi.pet", url: "https://bsky.app/profile/nekomimi.pet" },
112112+ { name: "Tangled", handle: "@nekomimi.pet", url: "https://tangled.org/@nekomimi.pet" },
113113+ { name: "Vgen", handle: "@ananekomimi", url: "https://vgen.co/ananekomimi" }
114114+]
115115+116116+export const sections = ["intro", "work", "connect"] as const
117117+118118+export type Section = (typeof sections)[number]
+29
src/frontend.tsx
···11+/**
22+ * This file is the entry point for the React app, it sets up the root
33+ * element and renders the App component to the DOM.
44+ *
55+ * It is included in `src/index.html`.
66+ */
77+88+import { StrictMode } from "react";
99+import { createRoot } from "react-dom/client";
1010+import { App } from "./App";
1111+import { AtProtoProvider } from "atproto-ui"
1212+1313+const elem = document.getElementById("root")!;
1414+const app = (
1515+ <StrictMode>
1616+ <AtProtoProvider>
1717+ <App />
1818+ </AtProtoProvider>
1919+ </StrictMode>
2020+);
2121+2222+if (import.meta.hot) {
2323+ // With hot module reloading, `import.meta.hot.data` is persisted.
2424+ const root = (import.meta.hot.data.root ??= createRoot(elem));
2525+ root.render(app);
2626+} else {
2727+ // The hot module reloading API is not available in production.
2828+ createRoot(elem).render(app);
2929+}
+37
src/hooks/useSystemTheme.ts
···11+import { useState, useEffect } from "react"
22+33+export function useSystemTheme() {
44+ const [isLightMode, setIsLightMode] = useState(false)
55+66+ useEffect(() => {
77+ const mediaQuery = window.matchMedia("(prefers-color-scheme: light)")
88+99+ const handleChange = (e: MediaQueryListEvent) => {
1010+ const isLight = e.matches
1111+ setIsLightMode(isLight)
1212+1313+ // Apply or remove the 'dark' class on the document element
1414+ if (isLight) {
1515+ document.documentElement.classList.remove('dark')
1616+ } else {
1717+ document.documentElement.classList.add('dark')
1818+ }
1919+ }
2020+2121+ const isLight = mediaQuery.matches
2222+ setIsLightMode(isLight)
2323+2424+ // Apply or remove the 'dark' class on initial load
2525+ if (isLight) {
2626+ document.documentElement.classList.remove('dark')
2727+ } else {
2828+ document.documentElement.classList.add('dark')
2929+ }
3030+3131+ mediaQuery.addEventListener("change", handleChange)
3232+3333+ return () => mediaQuery.removeEventListener("change", handleChange)
3434+ }, [])
3535+3636+ return isLightMode
3737+}