···1+# bun-react-tailwind-shadcn-template
2+3+To install dependencies:
4+5+```bash
6+bun install
7+```
8+9+To start a development server:
10+11+```bash
12+bun dev
13+```
14+15+To run for production:
16+17+```bash
18+bun start
19+```
20+21+This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···1+type DescriptionPart = {
2+ text: string
3+ bold?: boolean
4+ url?: string
5+}
6+7+export const personalInfo = {
8+ name: {
9+ first: "at://nekomimi.pet",
10+ last: "",
11+ },
12+ title: "Developer, cat",
13+ description: [
14+ { text: "A cat working on " },
15+ { text: "useful", bold: true },
16+ { text: ", " },
17+ { text: "genuine", bold: true },
18+ { text: " " },
19+ { text: "decentralized", bold: true, url: "https://atproto.com" },
20+ { text: " experiences. Not Web3. Also likes to " },
21+ { text: "draw", bold: true, url: "https://bsky.app/profile/art.nekomimi.pet" },
22+ { text: "." },
23+ ],
24+ availability: {
25+ status: "Available for work",
26+ location: "Richmond, VA, USA",
27+ },
28+ contact: {
29+ email: "ana@nekomimi.pet",
30+ },
31+}
32+33+export const currentRole = {
34+ title: "Freelance",
35+ company: "maybe your company :)",
36+ period: "2021 — Present",
37+}
38+39+export const skills = ["React", "TypeScript", "Go", "Devops/Infra", "Atproto", "Cryptocurrencies"]
40+41+export const workExperience = [
42+ {
43+ year: "2020-2025",
44+ role: "Fullstack Engineer, Infra/DevOps, Security Reviews",
45+ company: "Freelance",
46+ 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.",
47+ tech: ["React", "TypeScript", "Bun", "SQL"],
48+ projects: [
49+ {
50+ title: "atproto-ui",
51+ description: "A React component for rendering common UI elements across AT applications like Bluesky, Leaflet.pub, and more.",
52+ tech: ["React", "TypeScript"],
53+ links: {
54+ live: "https://atproto-ui.netlify.app/",
55+ github: "https://tangled.org/@nekomimi.pet/atproto-ui",
56+ },
57+ },
58+ {
59+ title: "wisp.place",
60+ description: "A static site hoster built on the AT protocol. Users retain control over site data and content while Wisp acts as a CDN.",
61+ tech: ["React", "TypeScript", "Bun", "ElysiaJS", "Hono", "Docker"],
62+ links: {
63+ live: "https://wisp.place",
64+ github: "#",
65+ },
66+ },
67+ {
68+ title: "Confidential",
69+ description: "NixOS consulting. Maintaining automated deployments, provisioning, and configuration management.",
70+ tech: ["Nix", "Terraform"],
71+ },
72+ {
73+ title: "Embedder",
74+ 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.",
75+ tech: ["HTMX", "Express", "TypeScript"],
76+ links: {
77+ github: "https://github.com/waveringana/embedder",
78+ },
79+ }
80+ ],
81+ },
82+ {
83+ year: "2016-2019",
84+ role: "Software Engineer, DevOps",
85+ company: "Horizen.io, later freelance",
86+ description: "Helped launch Horizen, built various necessary blockchain infrastructure like explorers and pools. Managed CI/CD pipelines, infrastructure, and community support.",
87+ tech: ["Node.js", "Jenkins", "C++"],
88+ projects: [
89+ {
90+ title: "Z-NOMP",
91+ description: "A port of NOMP for Equihash Coins, built using Node.js and Redis.",
92+ tech: ["Node.js", "Redis"],
93+ links: {
94+ github: "https://github.com/z-classic/z-nomp",
95+ },
96+ },
97+ {
98+ title: "Equihash-Solomining",
99+ description: "Pool software dedicated for Solomining, initially for equihash but for private clients, worked to adapt to PoW of their needs.",
100+ tech: ["Node.js", "Redis", "d3.js"],
101+ links: {
102+ github: "https://github.com/waveringana/equihash-solomining",
103+ },
104+ }
105+ ]
106+ },
107+]
108+109+export const socialLinks = [
110+ { name: "GitHub", handle: "@waveringana", url: "https://github.com/waveringana" },
111+ { name: "Bluesky", handle: "@nekomimi.pet", url: "https://bsky.app/profile/nekomimi.pet" },
112+ { name: "Tangled", handle: "@nekomimi.pet", url: "https://tangled.org/@nekomimi.pet" },
113+ { name: "Vgen", handle: "@ananekomimi", url: "https://vgen.co/ananekomimi" }
114+]
115+116+export const sections = ["intro", "work", "connect"] as const
117+118+export type Section = (typeof sections)[number]
+29
src/frontend.tsx
···00000000000000000000000000000
···1+/**
2+ * This file is the entry point for the React app, it sets up the root
3+ * element and renders the App component to the DOM.
4+ *
5+ * It is included in `src/index.html`.
6+ */
7+8+import { StrictMode } from "react";
9+import { createRoot } from "react-dom/client";
10+import { App } from "./App";
11+import { AtProtoProvider } from "atproto-ui"
12+13+const elem = document.getElementById("root")!;
14+const app = (
15+ <StrictMode>
16+ <AtProtoProvider>
17+ <App />
18+ </AtProtoProvider>
19+ </StrictMode>
20+);
21+22+if (import.meta.hot) {
23+ // With hot module reloading, `import.meta.hot.data` is persisted.
24+ const root = (import.meta.hot.data.root ??= createRoot(elem));
25+ root.render(app);
26+} else {
27+ // The hot module reloading API is not available in production.
28+ createRoot(elem).render(app);
29+}
+37
src/hooks/useSystemTheme.ts
···0000000000000000000000000000000000000
···1+import { useState, useEffect } from "react"
2+3+export function useSystemTheme() {
4+ const [isLightMode, setIsLightMode] = useState(false)
5+6+ useEffect(() => {
7+ const mediaQuery = window.matchMedia("(prefers-color-scheme: light)")
8+9+ const handleChange = (e: MediaQueryListEvent) => {
10+ const isLight = e.matches
11+ setIsLightMode(isLight)
12+13+ // Apply or remove the 'dark' class on the document element
14+ if (isLight) {
15+ document.documentElement.classList.remove('dark')
16+ } else {
17+ document.documentElement.classList.add('dark')
18+ }
19+ }
20+21+ const isLight = mediaQuery.matches
22+ setIsLightMode(isLight)
23+24+ // Apply or remove the 'dark' class on initial load
25+ if (isLight) {
26+ document.documentElement.classList.remove('dark')
27+ } else {
28+ document.documentElement.classList.add('dark')
29+ }
30+31+ mediaQuery.addEventListener("change", handleChange)
32+33+ return () => mediaQuery.removeEventListener("change", handleChange)
34+ }, [])
35+36+ return isLightMode
37+}