Got the homepage sorted

This commit is contained in:
2025-05-17 22:42:48 -06:00
parent b1cd87f20b
commit d86ae2f16b
49 changed files with 5226 additions and 1878 deletions

View File

@ -1,35 +0,0 @@
name: Docker Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.REPO_HOST }}
username: ${{ github.repository_owner }}
password: ${{ secrets.DEPLOY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.sha }}
${{ secrets.REPO_HOST }}/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest

31
.gitignore vendored
View File

@ -1,11 +1,24 @@
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# build output
dist/
# Fresh build directory
_fresh/
# npm dependencies
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@ -1,3 +0,0 @@
{
"deno.enable": true
}

View File

@ -1,35 +0,0 @@
FROM denoland/deno:alpine AS builder
WORKDIR /app
# Install build dependencies for native modules
RUN apk add --no-cache build-base python3
COPY . .
# Create node_modules directory and install dependencies
RUN deno cache -r main.ts
# Build Fresh application in a more controlled way (without task)
RUN deno run -A dev.ts build || deno run -A --unstable-worker-options --node-modules-dir main.ts build
FROM denoland/deno:alpine
WORKDIR /app
# Copy the Deno cache and node_modules
COPY --from=builder /deno-dir/ /deno-dir/
COPY --from=builder /app/node_modules/ /app/node_modules/
# Copy application code
COPY --from=builder /app/ /app/
# Ensure static assets directories permissions are set correctly
RUN chmod -R 755 /app/static /app/_fresh
ENV DENO_DEPLOYMENT=production
EXPOSE 8000
# Run with appropriate flags for static file serving
CMD ["run", "--allow-net", "--allow-read", "--allow-env", "--node-modules-dir", "main.ts"]

View File

@ -1,3 +1,48 @@
# Personal Site
# Astro Starter Kit: Basics
Re-written with Deno + Fresh :)
```sh
pnpm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `pnpm install` | Installs dependencies |
| `pnpm dev` | Starts local dev server at `localhost:4321` |
| `pnpm build` | Build your production site to `./dist/` |
| `pnpm preview` | Preview your build locally, before deploying |
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

23
astro.config.mjs Normal file
View File

@ -0,0 +1,23 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import preact from '@astrojs/preact';
import node from '@astrojs/node';
import icon from 'astro-icon';
// https://astro.build/config
export default defineConfig({
vite: {
plugins: [tailwindcss()]
},
integrations: [preact(), icon()],
adapter: node({
mode: 'standalone'
})
});

View File

@ -1,15 +0,0 @@
export default function HomeButtonLinks() {
return (
<div class="flex flex-row gap-4 text-3xl">
<a
href="/resume"
target="_blank"
rel="noopener noreferrer"
aria-label="React"
class="btn btn-dash btn-primary"
>
Resumé
</a>
</div>
);
}

View File

@ -1,37 +0,0 @@
import { LuArrowRight, LuClock } from "@preact-icons/lu";
import { Post } from "../lib/posts.ts";
export default function PostCard(props: { post: Post }) {
const { post } = props;
return (
<div class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title text-base-100 justify-center text-center break-words">
{post.title}
</h2>
<p class="text-center text-base-100 break-words">{post.blurb}</p>
<div class="flex flex-wrap items-center justify-center text-base-100 opacity-75 gap-2 text-sm sm:text-base">
<LuClock class="flex-shrink-0" />
<span>
{post.publishedAt!.toLocaleDateString("en-us", {
month: "long",
day: "numeric",
year: "numeric",
})}
</span>
</div>
<div class="card-actions justify-end mt-4">
<a
href={`/post/${post.slug}`}
class="btn btn-circle btn-sm sm:btn-md btn-primary text-accent"
>
<LuArrowRight class="text-lg" />
</a>
</div>
</div>
</div>
);
}

View File

@ -1,37 +0,0 @@
import { LuLink } from "@preact-icons/lu";
interface Project {
id: string;
name: string;
description: string;
link: string;
}
export default function ProjectCard(props: { project: Project }) {
const { project } = props;
return (
<div class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink">
<div class="card-body p-6">
<h2 class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100">
{project.name}
</h2>
<p class="text-center break-words my-4 text-base-100">
{project.description}
</p>
<div class="card-actions justify-end mt-4 ">
<a
href={project.link}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-secondary text-accent"
aria-label={`Visit ${project.name}`}
>
<LuLink class="text-lg" />
</a>
</div>
</div>
</div>
);
}

View File

@ -1,44 +0,0 @@
import { LuMail } from "@preact-icons/lu";
import { SiBluesky, SiForgejo, SiRss } from "@preact-icons/si";
export default function SocialLinks() {
return (
<div class="flex flex-row gap-4 text-xl sm:text-3xl">
<a
href="mailto:me@atri.dad"
aria-label="Email me"
class="hover:text-primary transition-colors"
>
<LuMail />
</a>
<a
href="/feed"
aria-label="RSS Feed"
class="hover:text-primary transition-colors"
>
<SiRss />
</a>
<a
href="https://git.atri.dad/atridad"
target="_blank"
rel="noopener noreferrer"
aria-label="Forgejo (Git)"
class="hover:text-primary transition-colors"
>
<SiForgejo />
</a>
<a
href="https://bsky.app/profile/atri.dad"
target="_blank"
rel="noopener noreferrer"
aria-label="Bluesky Profile"
class="hover:text-primary transition-colors"
>
<SiBluesky />
</a>
</div>
);
}

View File

@ -1,85 +0,0 @@
import {
SiDeno,
SiDocker,
SiGo,
SiPostgresql,
SiReact,
SiRedis,
SiTypescript,
} from "@preact-icons/si";
export default function TechLinks() {
return (
<div class="flex flex-row gap-4 text-xl sm:text-3xl">
<a
href="https://react.dev/"
target="_blank"
rel="noopener noreferrer"
aria-label="React"
class="hover:text-primary transition-colors"
>
<SiReact />
</a>
<a
href="https://www.typescriptlang.org/"
target="_blank"
rel="noopener noreferrer"
aria-label="TypeScript"
class="hover:text-primary transition-colors"
>
<SiTypescript />
</a>
<a
href="https://deno.com/"
target="_blank"
rel="noopener noreferrer"
aria-label="Deno"
class="hover:text-primary transition-colors"
>
<SiDeno />
</a>
<a
href="https://go.dev/"
target="_blank"
rel="noopener noreferrer"
aria-label="Go"
class="hover:text-primary transition-colors"
>
<SiGo />
</a>
<a
href="https://www.postgresql.org/"
target="_blank"
rel="noopener noreferrer"
aria-label="PostgreSQL"
class="hover:text-primary transition-colors"
>
<SiPostgresql />
</a>
<a
href="https://redis.io/"
target="_blank"
rel="noopener noreferrer"
aria-label="Redis"
class="hover:text-primary transition-colors"
>
<SiRedis />
</a>
<a
href="https://www.docker.com/"
target="_blank"
rel="noopener noreferrer"
aria-label="Docker"
class="hover:text-primary transition-colors"
>
<SiDocker />
</a>
</div>
);
}

View File

@ -1,32 +0,0 @@
{
"nodeModulesDir": "auto",
"lock": false,
"tasks": {
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
"manifest": "deno task cli manifest $(pwd)",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
},
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"exclude": ["**/_fresh/*"],
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
"@deno/gfm": "jsr:@deno/gfm@^0.11.0",
"@pakornv/fresh-plugin-tailwindcss": "jsr:@pakornv/fresh-plugin-tailwindcss@^1.0.2",
"@preact-icons/lu": "jsr:@preact-icons/lu@^1.0.13",
"@preact-icons/si": "jsr:@preact-icons/si@^1.0.13",
"@std/front-matter": "jsr:@std/front-matter@^1.0.9",
"@std/path": "jsr:@std/path@^1.0.9",
"@tailwindcss/typography": "npm:@tailwindcss/typography@^0.5.16",
"daisyui": "npm:daisyui@^5.0.35",
"preact": "npm:preact@10.26.6",
"@preact/signals": "npm:@preact/signals@1.2.2",
"@preact/signals-core": "npm:@preact/signals-core@1.5.1",
"$std/": "https://deno.land/std@0.216.0/",
"tailwindcss": "npm:tailwindcss@^4.1.5"
},
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" }
}

8
dev.ts
View File

@ -1,8 +0,0 @@
#!/usr/bin/env -S deno run -A --watch=static/,routes/
import dev from "$fresh/dev.ts";
import config from "./fresh.config.ts";
import "$std/dotenv/load.ts";
await dev(import.meta.url, "./main.ts", config);

View File

@ -1,10 +0,0 @@
version: '3.8'
services:
app:
image: ${IMAGE:-ghcr.io/yourusername/your-fresh-project:latest}
restart: unless-stopped
environment:
- DENO_DEPLOYMENT=production
ports:
- "3000:8000"

View File

@ -1,6 +0,0 @@
import { defineConfig } from "$fresh/server.ts";
import tailwind from "@pakornv/fresh-plugin-tailwindcss";
export default defineConfig({
plugins: [tailwind()],
});

View File

@ -1,43 +0,0 @@
// DO NOT EDIT. This file is generated by Fresh.
// This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`.
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $_layout from "./routes/_layout.tsx";
import * as $api_chat from "./routes/api/chat.ts";
import * as $api_ping from "./routes/api/ping.ts";
import * as $chat from "./routes/chat.tsx";
import * as $index from "./routes/index.tsx";
import * as $post_slug_ from "./routes/post/[slug].tsx";
import * as $posts from "./routes/posts.tsx";
import * as $projects from "./routes/projects.tsx";
import * as $resume from "./routes/resume.tsx";
import * as $Chat from "./islands/Chat.tsx";
import * as $NavigationBar from "./islands/NavigationBar.tsx";
import * as $ScrollUpButton from "./islands/ScrollUpButton.tsx";
import type { Manifest } from "$fresh/server.ts";
const manifest = {
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/_layout.tsx": $_layout,
"./routes/api/chat.ts": $api_chat,
"./routes/api/ping.ts": $api_ping,
"./routes/chat.tsx": $chat,
"./routes/index.tsx": $index,
"./routes/post/[slug].tsx": $post_slug_,
"./routes/posts.tsx": $posts,
"./routes/projects.tsx": $projects,
"./routes/resume.tsx": $resume,
},
islands: {
"./islands/Chat.tsx": $Chat,
"./islands/NavigationBar.tsx": $NavigationBar,
"./islands/ScrollUpButton.tsx": $ScrollUpButton,
},
baseUrl: import.meta.url,
} satisfies Manifest;
export default manifest;

View File

@ -1,161 +0,0 @@
import { useEffect, useState } from "preact/hooks";
interface ChatMessage {
text: string;
sender: string;
timestamp: string;
}
export default function Chat() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [newMessage, setNewMessage] = useState("");
const [username, setUsername] = useState("");
const [socket, setSocket] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [userCount, setUserCount] = useState(0);
useEffect(() => {
if (!username) {
const randomNum = Math.floor(Math.random() * 10000);
setUsername(`HumanGuest${randomNum}`);
}
const wsProtocol = globalThis.location.protocol === "https:"
? "wss:"
: "ws:";
const ws = new WebSocket(
`${wsProtocol}//${globalThis.location.host}/api/chat`,
);
ws.onopen = () => {
console.log("Connected to chat");
setIsConnected(true);
setSocket(ws);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === "user_count") {
setUserCount(data.count);
} else {
setMessages((prev) => [...prev, data]);
// Auto-scroll to bottom on new message
const chatBox = document.getElementById("chat-messages");
if (chatBox) {
setTimeout(() => {
chatBox.scrollTop = chatBox.scrollHeight;
}, 50);
}
}
} catch (err) {
console.error("Error processing message:", err);
}
};
ws.onclose = () => {
console.log("Disconnected from chat");
setIsConnected(false);
};
return () => {
ws.close();
};
}, []);
const sendMessage = (e: Event) => {
e.preventDefault();
if (!newMessage.trim() || !socket) return;
const messageData = {
text: newMessage,
sender: username,
timestamp: new Date().toISOString(),
};
socket.send(JSON.stringify(messageData));
setNewMessage("");
};
return (
<div class="w-full max-w-4xl mx-auto bg-[#1E2127] rounded-lg shadow-lg overflow-hidden border border-gray-800 flex flex-col h-[70vh]">
{/* Header */}
<div class="p-4 bg-secondary text-white">
<h2 class="text-2xl font-bold">Live Chat</h2>
<p class="text-sm">
{isConnected
? `${userCount} online • Messages are not saved`
: "Connecting..."}
</p>
</div>
<div
id="chat-messages"
class="flex-grow overflow-y-auto bg-[#1E2127] text-gray-300 p-4"
>
{messages.length === 0
? (
<p class="text-center text-gray-500 py-8">
No messages yet.
</p>
)
: (
messages.map((msg, i) => (
<div
key={i}
class={`mb-3 max-w-[85%] ${
msg.sender === username ? "ml-auto" : ""
}`}
>
<div
class={`px-4 py-2 rounded-lg ${
msg.sender === username
? "bg-secondary text-white rounded-br-none"
: "bg-gray-800 text-gray-200 rounded-bl-none"
}`}
>
<div class="flex justify-between items-baseline mb-1">
<span class="font-bold text-sm">
{msg.sender === username ? "You" : msg.sender}
</span>
<span class="text-xs opacity-70 ml-2">
{new Date(msg.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
<p class="break-words">{msg.text}</p>
</div>
</div>
))
)}
</div>
<div class="p-3 border-t border-gray-800 pb-6 md:pb-3">
<form onSubmit={sendMessage} class="relative">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.currentTarget.value)}
placeholder="Type your message..."
class="w-full pl-4 pr-20 py-3 bg-gray-800 text-white rounded-lg border-0 focus:outline-none focus:ring-1 focus:ring-secondary placeholder-gray-500"
disabled={!isConnected}
/>
<button
type="submit"
class="absolute right-0 top-0 h-full bg-secondary text-white px-5 rounded-r-lg font-medium"
disabled={!isConnected || !newMessage.trim()}
>
Send
</button>
</form>
<p class="mt-2 text-xs text-gray-500">
You are connected as{" "}
<span class="font-medium text-gray-400">{username}</span>
</p>
</div>
</div>
);
}

View File

@ -1,45 +0,0 @@
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
import { LuArrowUp } from "@preact-icons/lu";
export default function ScrollUpButton() {
const isVisible = useSignal(false);
useEffect(() => {
const checkScroll = () => {
isVisible.value = globalThis.scrollY > 300;
};
checkScroll();
globalThis.addEventListener("scroll", checkScroll);
return () => {
globalThis.removeEventListener("scroll", checkScroll);
};
}, []);
const scrollToTop = () => {
globalThis.scrollTo({
top: 0,
behavior: "smooth",
});
};
return (
<button
type="button"
onClick={scrollToTop}
class={`fixed bottom-20 right-4 z-20 bg-secondary hover:bg-primary
p-3 rounded-full shadow-lg transition-all duration-300
${
isVisible.value
? "opacity-70 translate-y-0"
: "opacity-0 translate-y-10 pointer-events-none"
}`}
aria-label="Scroll to top"
>
<LuArrowUp class="text-lg" />
</button>
);
}

View File

@ -1,45 +0,0 @@
import { extractYaml } from "@std/front-matter";
import { join } from "@std/path";
const POSTS_DIR = "./posts";
interface FrontMatter {
title: string;
published_at: string;
blurb: string;
}
export interface Post {
slug: string;
title: string;
publishedAt: Date | null;
blurb: string;
content: string;
}
export async function getPost(slug: string): Promise<Post> {
const text = await Deno.readTextFile(join(POSTS_DIR, `${slug}.md`));
const { attrs, body } = extractYaml<FrontMatter>(text);
const post = {
slug,
title: attrs.title,
publishedAt: attrs.published_at ? new Date(attrs.published_at) : null,
blurb: attrs.blurb,
content: body,
};
return post;
}
export async function getPosts(): Promise<Post[]> {
const files = Deno.readDir(POSTS_DIR);
const promises = [];
for await (const file of files) {
if (file.name.startsWith(".")) continue;
const slug = file.name.replace(".md", "");
promises.push(getPost(slug));
}
const posts = (await Promise.all(promises) as Post[])
.filter((post) => post.publishedAt instanceof Date);
posts.sort((a, b) => b.publishedAt!.getTime() - a.publishedAt!.getTime());
return posts;
}

13
main.ts
View File

@ -1,13 +0,0 @@
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import "$std/dotenv/load.ts";
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import config from "./fresh.config.ts";
await start(manifest, config);

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^9.2.1",
"@astrojs/preact": "^4.0.11",
"@preact/signals": "^2.0.4",
"@tailwindcss/vite": "^4.1.7",
"astro": "^5.7.13",
"astro-icon": "^1.1.5",
"lucide-preact": "^0.511.0",
"preact": "^10.26.6",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/simple-icons": "^1.2.34",
"daisyui": "^5.0.35"
}
}

4881
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
---
title: Current List of Favourite Tools
published_at: 2025-01-28T15:00:00.000Z
---
I change what I use _constantly_ in order to find something that feels just
right. I wanted to share them here and update them here so when someone asks, I
can just point them to this article.
1. Sublime Text - Currently my favourite text editor. Fast, simple, and
extensible. Just a joy to use!
2. Sublime Merge - Honestly one of the fastest and best looking git GUIs around!
Awesome for visualizing changes when you have larger code changes.
3. Ghostty - A Zig based terminal emulator by one of the founders of Hashicorp.
Runs great on MacOS and Linux. No windows for those who are into that.
4. OrbStack - A faster alternative to Docker Desktop that also runs VMs!
5. Bitwarden - An open-source password manager. Easy to self host with
Vaultwarden and with the recent updates, it has SSH Agent support!
6. iA Writer - A minimalist Markdown editor. For MacOS and Windows only, but
really the MacOS version is the most mature. Awesome for focus.
7. Dataflare - A simple but powerful cross-platform database client. Supports
most common databases, including LibSQL which is rare!
8. Bruno - A simple and powerful API client, similar to Postman. An critical
tool to debug API endpoints.
I hope you found this helpful! This will be periodically updated to avoid
outdated recommendations.

View File

@ -1,58 +0,0 @@
---
title: Re-write it in Deno!
published_at: 2025-04-25T15:00:00.000Z
---
So... all new site! I use this site as something of an experiment for whatever
technology interests me. One recently got my attention:
_Deno!_
Yes... Deno the friendly re-imagining of Node.js by Ryan Dahl, the the creator
of Node.js! Now given my recent dive into Golang and HTMX, this might seem odd.
Let me explain:
## Built in TSX and Typescript support!
Deno has built in Typescript support, which has been huge for me. Lets table the
whole "Bun and new Node can do that too" discussion for now. Its simply magical
to have a Typescript codebase without a single tsconfig in sight!
## Security!
Look, the secrity model of Deno is incredible. The idea that the runtime will
default-deny permissions unless you as the developer enables them is an awesome
move.
## Imports
Being able to seamlessly pull from JSR and NPM is amazing! Having those two
ecosystems work without the frustrating package.json dance is refreshing. I
mean, just being able to import directly from URLs? Game changer!
## Built-in tooling
The tooling that ships with Deno is first-class! Formatter, linter, test runner,
doc generator... all built right in! No more spending half a day configuring
eslint, prettier, and jest just to get started on a project. Just use
`deno fmt`, `deno lint`, `deno test` and you're good to go.
## Web standards first
Fetch, web streams, event listeners and so on all work just like they do in the
browser. Coming from Node where you have to remember different APIs between
back-end and front-end JS, this is absolutely a breath of fresh air.
## What about Golang? GOTH Stack?
I still love Go as a language and will continue to use it for certain things.
Its incredibly fast and the concurrency model is awesome, but sometimes you just
want to put together a quick web application and JS frameworks are just much
easier to reach for.
## Whats next?
I'm rebuilding this entire site with Deno Fresh, which is their web framework.
It has their island architecture which means minimal JS sent to the client. Will
I stick with it? Who knows! All I know is this has definitely been a blast of a
re-write.

View File

@ -1,10 +0,0 @@
---
title: Welcome!
published_at: 2024-10-20T15:00:00.000Z
---
Welcome to my site! This is a place for me to share my thoughts and updates on
my projects. I hope you find something interesting here.
Feel free to reach out if you have any questions or comments. I'd love to hear
from you! I can be reached by email at [me@atri.dad](mailto:me@atri.dad).

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -1,12 +0,0 @@
import { Head } from "$fresh/runtime.ts";
export default function Error404() {
return (
<>
<Head>
<title>404 - Page not found</title>
</Head>
<h1>404 Page Not Found</h1>
</>
);
}

View File

@ -1,17 +0,0 @@
import { type PageProps } from "$fresh/server.ts";
export default function App({ Component }: PageProps) {
return (
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Atridad Lahiji</title>
<link rel="stylesheet" href="/styles.css" />
<link rel="icon" type="image/x-icon" href="/favicon.ico"></link>
</head>
<body>
<Component />
</body>
</html>
);
}

View File

@ -1,27 +0,0 @@
import { PageProps } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts";
import NavigationBar from "../islands/NavigationBar.tsx";
import ScrollUpButton from "../islands/ScrollUpButton.tsx";
export default function Layout({ Component, url }: PageProps) {
const currentPath = url.pathname;
return (
<>
<Head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<title>Atridad Lahiji</title>
</Head>
<body class="flex flex-col min-h-screen">
<main class="flex-grow flex flex-col gap-4 items-center justify-center">
<Component />
</main>
<NavigationBar currentPath={currentPath} />
<ScrollUpButton />
</body>
</>
);
}

View File

@ -1,85 +0,0 @@
import { FreshContext } from "$fresh/server.ts";
const chatConnections = new Set<WebSocket>();
// HTML sanitization
function sanitizeText(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Process and sanitize message object
function processChatMessage(message: string): string {
try {
const parsed = JSON.parse(message);
if (typeof parsed.text === "string") {
parsed.text = sanitizeText(parsed.text.trim().slice(0, 2000));
}
if (typeof parsed.sender === "string") {
parsed.sender = sanitizeText(parsed.sender.trim().slice(0, 50));
}
if (!parsed.timestamp) {
parsed.timestamp = new Date().toISOString();
}
return JSON.stringify(parsed);
} catch (error) {
console.error("Invalid message format:", error);
return JSON.stringify({
text: "Error: Invalid message format",
sender: "System",
timestamp: new Date().toISOString(),
});
}
}
// Broadcast current user count to all clients
function broadcastUserCount() {
const count = chatConnections.size;
const message = JSON.stringify({
type: "user_count",
count: count,
});
for (const client of chatConnections) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
}
export const handler = (req: Request, _ctx: FreshContext): Response => {
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = () => {
chatConnections.add(socket);
console.log(`New connection: ${chatConnections.size} users connected`);
broadcastUserCount();
};
// Handle messages
socket.onmessage = (event) => {
const sanitizedMessage = processChatMessage(event.data);
for (const client of chatConnections) {
if (client.readyState === WebSocket.OPEN) {
client.send(sanitizedMessage);
}
}
};
socket.onclose = () => {
chatConnections.delete(socket);
console.log(`Connection closed: ${chatConnections.size} users connected`);
broadcastUserCount();
};
return response;
};

View File

@ -1,5 +0,0 @@
import { FreshContext } from "$fresh/server.ts";
export const handler = (_req: Request, _ctx: FreshContext): Response => {
return new Response("pong");
};

View File

@ -1,25 +0,0 @@
import Chat from "../islands/Chat.tsx";
export default function ChatPage() {
return (
<div class="min-h-screen p-4 pb-24">
<div class="flex items-center justify-center mb-6">
<h1 class="text-3xl font-bold text-secondary">Chat Room</h1>
<span class="ml-3 border border-pink-500 text-pink-500 rounded-full px-3 py-1 text-sm">
Demo
</span>
</div>
<div class="max-w-4xl mx-auto">
<Chat />
</div>
<div class="mt-4 text-center text-xs text-gray-500">
<p>
This is an ephemeral chat room. Messages are only visible to users
currently online and aren't stored after you leave.
</p>
</div>
</div>
);
}

View File

@ -1,80 +0,0 @@
import { render } from "@deno/gfm";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getPost, Post } from "../../lib/posts.ts";
import { LuClock } from "@preact-icons/lu";
export const handler: Handlers<Post> = {
async GET(_req, ctx) {
try {
const post = await getPost(ctx.params.slug);
return post.publishedAt ? ctx.render(post) : ctx.renderNotFound();
} catch (err) {
console.error(err);
return ctx.renderNotFound();
}
},
};
export default function PostPage(props: PageProps<Post>) {
const post = props.data;
return (
<>
<div class="min-h-screen p-4 md:p-8">
<div class="max-w-3xl mx-auto">
<div class="p-4 md:p-8">
{/* Header section */}
<h1 class="text-4xl md:text-5xl font-bold text-primary mb-6">
{post.title}
</h1>
<div class="flex flex-wrap items-center gap-4 mb-6">
{/* Date with clock icon */}
<div class="flex items-center flex-row gap-2 text-base-content opacity-75">
<LuClock />
<time>
{post.publishedAt!.toLocaleDateString("en-us", {
month: "long",
day: "numeric",
year: "numeric",
})}
</time>
</div>
{/* Back button */}
<a href="/posts" class="btn btn-outline btn-primary btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 17l-5-5m0 0l5-5m-5 5h12"
/>
</svg>
Back
</a>
</div>
{/* Divider line */}
<div class="divider"></div>
{/* Content section */}
<div
class="max-w-none prose"
data-color-mode="dark"
data-dark-theme="dark"
// deno-lint-ignore react-no-danger
dangerouslySetInnerHTML={{ __html: render(post.content) }}
/>
</div>
</div>
</div>
</>
);
}

View File

@ -1,24 +0,0 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { getPosts, Post } from "../lib/posts.ts";
import PostCard from "../components/PostCard.tsx";
export const handler: Handlers<Post[]> = {
async GET(_req, ctx) {
const posts = await getPosts();
return ctx.render(posts);
},
};
export default function PostsPage(props: PageProps<Post[]>) {
const posts = props.data;
return (
<div class="min-h-screen p-4 sm:p-8">
<h1 class="text-3xl sm:text-4xl font-bold text-primary mb-6 sm:mb-8 text-center">
Posts
</h1>
<div class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto">
{posts.map((post) => <PostCard key={post.slug} post={post} />)}
</div>
</div>
);
}

View File

@ -1,60 +0,0 @@
import ProjectCard from "../components/ProjectCard.tsx";
interface Project {
id: string;
name: string;
description: string;
link: string;
}
export default function ProjectsPage() {
const projects: Project[] = [
{
id: "bluesky-pds-manager",
name: "BlueSky PDS Manager",
description:
"A web-based BlueSky PDS Manager. Manage your invite codes and users with a simple web UI.",
link: "https://pdsman.atri.dad",
},
{
id: "pollo",
name: "Pollo",
description: "A dead-simple real-time voting tool.",
link: "https://git.atri.dad/atridad/pollo",
},
{
id: "goth-stack",
name: "GOTH Stack",
description:
"🚀 A Web Application Template Powered by HTMX + Go + Tailwind 🚀",
link: "https://git.atri.dad/atridad/goth.stack",
},
{
id: "himbot",
name: "Himbot",
description:
"A discord bot written in Go. Loosly named after my username online (HimbothySwaggins).",
link: "https://git.atri.dad/atridad/himbot",
},
{
id: "loadr",
name: "loadr",
description:
"A lightweight REST load testing tool with robust support for different verbs, token auth, and performance reports.",
link: "https://git.atri.dad/atridad/loadr",
},
];
return (
<div class="min-h-screen p-4 sm:p-8">
<h1 class="text-3xl sm:text-4xl font-bold text-secondary mb-6 sm:mb-8 text-center">
Projects
</h1>
<div class="flex flex-row flex-wrap justify-center gap-4 sm:gap-6 max-w-6xl mx-auto">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</div>
);
}

View File

@ -1,239 +0,0 @@
import { Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import {
LuMail,
LuGithub,
LuLinkedin,
LuGlobe,
LuGitBranch,
LuDownload,
} from "@preact-icons/lu";
interface ResumeData {
basics: {
name: string;
email: string;
url?: { href: string };
};
sections: {
summary: { name: string; content: string };
profiles: { name: string; items: { network: string; username: string; url: { href: string } }[] };
skills: { name: string; items: { id: string; name: string; level: number }[] };
experience: { name: string; items: { id: string; company: string; position: string; date: string; location: string; summary: string; url?: { href: string } }[] };
education: { name: string; items: { id: string; institution: string; studyType: string; area: string; date: string; summary: string }[] };
volunteer: { name: string; items: { id: string; organization: string; position: string; date: string /* No summary here */ }[] };
};
}
export const handler: Handlers<ResumeData> = {
async GET(_req, ctx) {
try {
const resp = await fetch(new URL("/files/resume.json", ctx.url).href);
if (!resp.ok) {
console.error(`Error fetching resume.json: ${resp.status} ${resp.statusText}`);
return ctx.render(undefined);
}
const resumeData: ResumeData = await resp.json();
const skillsSection = resumeData.sections.skills;
if (skillsSection && skillsSection.items) {
const tsSkill = skillsSection.items.find(s => s.name === "Typescrpt");
if (tsSkill) {
tsSkill.name = "Typescript";
}
}
return ctx.render(resumeData);
} catch (error) {
console.error("Error processing resume data:", error);
return ctx.render(undefined);
}
},
};
export default function ResumePage({ data }: PageProps<ResumeData | undefined>) {
if (!data) {
return (
<>
<Head><title>Error Loading Resume</title></Head>
<div class="container mx-auto p-4 max-w-4xl text-center">
<h1 class="text-2xl font-bold text-error">Error loading resume data.</h1>
<p>Please try refreshing the page.</p>
</div>
</>
);
}
const { basics, sections } = data;
const { summary, profiles, skills, experience, education, volunteer } = sections;
return (
<>
<Head>
<title>{basics.name} - Resume</title>
<meta name="description" content={`${basics.name}'s professional resume.`} />
</Head>
<div class="container mx-auto p-4 max-w-4xl">
<h1 class="text-4xl font-bold mb-6 text-center">{basics.name}</h1>
{/* Contact Info */}
<div class="flex justify-center items-center flex-wrap gap-x-4 gap-y-2 mb-6">
{basics.email && (
<a href={`mailto:${basics.email}`} class="link link-hover inline-flex items-center gap-1">
<LuMail /> {basics.email}
</a>
)}
{profiles.items.find(p => p.network === "GitHub") && (
<a href={profiles.items.find(p => p.network === "GitHub")!.url.href} target="_blank" rel="noopener noreferrer" class="link link-hover inline-flex items-center gap-1">
<LuGithub /> GitHub
</a>
)}
{profiles.items.find(p => p.network === "linkedin") && (
<a href={profiles.items.find(p => p.network === "linkedin")!.url.href} target="_blank" rel="noopener noreferrer" class="link link-hover inline-flex items-center gap-1">
<LuLinkedin /> LinkedIn
</a>
)}
</div>
{/* Download Resume Button */}
<div class="text-center mb-8">
<a
href="/files/Atridad_Lahiji_Resume_Public.pdf"
download="Atridad_Lahiji_Resume.pdf"
class="btn btn-primary inline-flex items-center gap-2"
>
<LuDownload /> Download Resume (PDF)
</a>
</div>
{/* Summary Card */}
{summary && (
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl">{summary.name || "Summary"}</h2>
<div dangerouslySetInnerHTML={{ __html: summary.content }}></div>
</div>
</div>
)}
{/* Profiles Card */}
{profiles && profiles.items && profiles.items.length > 0 && (
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl">{profiles.name || "Profiles"}</h2>
<div class="flex flex-wrap gap-4">
{profiles.items.map((profile) => {
let IconComponent = LuGlobe;
const networkLower = profile.network.toLowerCase();
if (networkLower === "github") IconComponent = LuGithub;
else if (networkLower === "linkedin") IconComponent = LuLinkedin;
else if (networkLower === "forgejo") IconComponent = LuGitBranch;
return (
<a
key={profile.network}
href={profile.url.href}
target="_blank"
rel="noopener noreferrer"
class="link link-hover inline-flex items-center gap-1"
>
<IconComponent /> {profile.network} ({profile.username})
</a>
);
})}
</div>
</div>
</div>
)}
{/* Skills Card */}
{skills && skills.items && skills.items.length > 0 && (
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl">{skills.name || "Skills"}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{skills.items.map((skill) => (
<div key={skill.id || skill.name}>
<label class="label">
<span class="label-text">{skill.name}</span>
</label>
<progress
class="progress progress-primary w-full"
value={skill.level * 20}
max="100"
>
</progress>
</div>
))}
</div>
</div>
</div>
)}
{/* Experience Card */}
{experience && experience.items && experience.items.length > 0 && (
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl">{experience.name || "Experience"}</h2>
<div class="space-y-4">
{experience.items.map((exp, index) => (
<div key={exp.id || index} class="collapse collapse-arrow bg-base-100">
<input type="radio" name="resume-accordion-experience" checked={index === 0} readOnly />
<div class="collapse-title text-xl font-medium">
{exp.position} at {exp.company} ({exp.date})
{exp.location && (
<span class="text-sm font-normal float-right pt-1">{exp.location}</span>
)}
</div>
<div class="collapse-content">
{exp.url && exp.url.href && (
<a href={exp.url.href} target="_blank" rel="noopener noreferrer" class="link link-primary block mb-2">{exp.url.href}</a>
)}
<div dangerouslySetInnerHTML={{ __html: exp.summary }}></div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Education Card */}
{education && education.items && education.items.length > 0 && (
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl">{education.name || "Education"}</h2>
<div class="space-y-4">
{education.items.map((edu, index) => (
<div key={edu.id || index}>
<h3 class="text-lg font-semibold">{edu.institution}</h3>
<p>{edu.studyType} - {edu.area} ({edu.date})</p>
{edu.summary && (
<div class="ml-4 text-sm mt-1" dangerouslySetInnerHTML={{ __html: edu.summary }}></div>
)}
</div>
))}
</div>
</div>
</div>
)}
{/* Volunteering Card */}
{volunteer && volunteer.items && volunteer.items.length > 0 && (
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl">{volunteer.name || "Volunteering"}</h2>
<div class="space-y-4">
{volunteer.items.map((vol, index) => (
<div key={vol.id || index}>
<h3 class="text-lg font-semibold">{vol.organization}</h3>
<p>{vol.position} ({vol.date})</p>
</div>
))}
</div>
</div>
</div>
)}
</div>
</>
);
}

View File

@ -1,12 +1,6 @@
import { useComputed, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
import {
LuCodeXml,
LuHouse,
LuMessageCircle,
LuNotebookPen,
LuFileText,
} from "@preact-icons/lu";
import { Home, NotebookPen, FileText, CodeXml, MessageCircle } from 'lucide-preact';
interface NavigationBarProps {
currentPath: string;
@ -27,7 +21,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
};
useEffect(() => {
let scrollTimer: number | undefined;
let scrollTimer: ReturnType<typeof setTimeout> | undefined;
const handleScroll = () => {
isScrolling.value = true;
@ -59,7 +53,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
<li class="mx-1">
<a href="/" class={currentPath === "/" ? "menu-active" : ""}>
<div class="tooltip" data-tip="Home">
<LuHouse class="text-xl" />
<Home />
</div>
</a>
</li>
@ -70,7 +64,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
class={isPostsPath(currentPath) ? "menu-active" : ""}
>
<div class="tooltip" data-tip="Posts">
<LuNotebookPen class="text-xl" />
<NotebookPen />
</div>
</a>
</li>
@ -81,7 +75,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
class={currentPath === "/resume" ? "menu-active" : ""}
>
<div class="tooltip" data-tip="Resume">
<LuFileText class="text-xl" />
<FileText />
</div>
</a>
</li>
@ -92,7 +86,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
class={currentPath.startsWith("/projects") ? "menu-active" : ""}
>
<div class="tooltip" data-tip="Projects">
<LuCodeXml class="text-xl" />
<CodeXml />
</div>
</a>
</li>
@ -103,7 +97,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
class={currentPath.startsWith("/chat") ? "menu-active" : ""}
>
<div class="tooltip" data-tip="Chat">
<LuMessageCircle class="text-xl" />
<MessageCircle />
</div>
</a>
</li>

View File

@ -0,0 +1,41 @@
---
import { Icon } from 'astro-icon/components';
---
<div class="flex flex-row gap-4 text-xl sm:text-3xl">
<a
href="mailto:me@atri.dad"
aria-label="Email me"
class="hover:text-primary transition-colors"
>
<Icon name="mdi:email" />
</a>
<a
href="/feed"
aria-label="RSS Feed"
class="hover:text-primary transition-colors"
>
<Icon name="mdi:rss" />
</a>
<a
href="https://git.atri.dad/atridad"
target="_blank"
rel="noopener noreferrer"
aria-label="Forgejo (Git)"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:gitea" />
</a>
<a
href="https://bsky.app/profile/atri.dad"
target="_blank"
rel="noopener noreferrer"
aria-label="Bluesky Profile"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:bluesky" />
</a>
</div>

View File

@ -0,0 +1,74 @@
---
import { Icon } from 'astro-icon/components';
---
<div class="flex flex-row gap-4 text-xl sm:text-3xl">
<a
href="https://react.dev/"
target="_blank"
rel="noopener noreferrer"
aria-label="React"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:react" />
</a>
<a
href="https://www.typescriptlang.org/"
target="_blank"
rel="noopener noreferrer"
aria-label="TypeScript"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:typescript" />
</a>
<a
href="https://astro.build/"
target="_blank"
rel="noopener noreferrer"
aria-label="Deno"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:astro" />
</a>
<a
href="https://go.dev/"
target="_blank"
rel="noopener noreferrer"
aria-label="Go"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:go" />
</a>
<a
href="https://www.postgresql.org/"
target="_blank"
rel="noopener noreferrer"
aria-label="PostgreSQL"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:postgresql" />
</a>
<a
href="https://redis.io/"
target="_blank"
rel="noopener noreferrer"
aria-label="Redis"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:redis" />
</a>
<a
href="https://www.docker.com/"
target="_blank"
rel="noopener noreferrer"
aria-label="Docker"
class="hover:text-primary transition-colors"
>
<Icon name="simple-icons:docker" />
</a>
</div>

26
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,26 @@
---
import NavigationBar from "../components/NavigationBar";
const currentPath = Astro.url.pathname;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="generator" content={Astro.generator} />
<title>Atridad Lahiji</title>
</head>
<body>
</body>
<body class="flex flex-col min-h-screen">
<main class="flex-grow flex flex-col gap-4 items-center justify-center">
<slot />
</main>
<NavigationBar currentPath={currentPath} />
<!-- <ScrollUpButton /> -->
</body>
</html>

View File

@ -1,11 +1,12 @@
import HomeButtonLinks from "../components/HomeButtonLinks.tsx";
import SocialLinks from "../components/SocialLinks.tsx";
import TechLinks from "../components/TechLinks.tsx";
---
import SocialLinks from '../components/SocialLinks.astro';
import TechLinks from '../components/TechLinks.astro';
import Layout from '../layouts/Layout.astro';
import '../styles/global.css';
---
export default function Home() {
return (
<>
<img
<Layout>
<img
src="/logo.webp"
alt="A drawing of Atridad Lahiji by Shelze!"
height={150}
@ -28,7 +29,5 @@ export default function Home() {
<TechLinks />
<HomeButtonLinks />
</>
);
}
<!-- <HomeButtonLinks /> -->
</Layout>

36
src/styles/global.css Normal file
View File

@ -0,0 +1,36 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "chaoticbisexual";
default: true;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(25.33% 0.016 252.42);
--color-base-200: oklch(23.26% 0.014 253.1);
--color-base-300: oklch(21.15% 0.012 254.09);
--color-base-content: oklch(97.807% 0.029 256.847);
--color-primary: oklch(65% 0.241 354.308);
--color-primary-content: oklch(96% 0.018 272.314);
--color-secondary: oklch(60% 0.25 292.717);
--color-secondary-content: oklch(94% 0.028 342.258);
--color-accent: oklch(78% 0.154 211.53);
--color-accent-content: oklch(38% 0.063 188.416);
--color-neutral: oklch(40% 0.17 325.612);
--color-neutral-content: oklch(92% 0.004 286.32);
--color-info: oklch(74% 0.16 232.661);
--color-info-content: oklch(29% 0.066 243.157);
--color-success: oklch(76% 0.177 163.223);
--color-success-content: oklch(37% 0.077 168.94);
--color-warning: oklch(82% 0.189 84.429);
--color-warning-content: oklch(41% 0.112 45.904);
--color-error: oklch(71% 0.194 13.428);
--color-error-content: oklch(27% 0.105 12.094);
--radius-selector: 1rem;
--radius-field: 1rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 1;
}

View File

@ -1,483 +0,0 @@
{
"basics": {
"url": {
"href": "https://atri.dad",
"label": ""
},
"name": "Atridad Lahiji",
"email": "me@atri.dad",
"phone": "",
"picture": {
"url": "",
"size": 64,
"effects": {
"border": false,
"hidden": false,
"grayscale": false
},
"aspectRatio": 1,
"borderRadius": 0
},
"headline": "",
"location": "",
"customFields": []
},
"metadata": {
"css": {
"value": ".text-2xl {\n\tfont-size: 30px;\n}",
"visible": true
},
"page": {
"format": "letter",
"margin": 16,
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"notes": "",
"theme": {
"text": "#000000",
"primary": "#0284c7",
"background": "#ffffff"
},
"layout": [
[
[
"summary",
"education",
"experience",
"projects",
"references",
"custom.b5li7wh27iylvqlsmeavvkzh"
],
[
"profiles",
"skills",
"volunteer",
"interests",
"certifications",
"awards",
"publications",
"languages"
]
]
],
"template": "glalie",
"typography": {
"font": {
"size": 14,
"family": "Lato",
"subset": "latin",
"variants": [
"regular"
]
},
"hideIcons": false,
"lineHeight": 0.95,
"underlineLinks": true
}
},
"sections": {
"awards": {
"id": "awards",
"name": "Awards",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
},
"custom": {
"b5li7wh27iylvqlsmeavvkzh": {
"id": "b5li7wh27iylvqlsmeavvkzh",
"name": "Custom Section",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
}
},
"skills": {
"id": "skills",
"name": "Skills",
"items": [
{
"id": "lpwyb43emmmukje3c49yupu7",
"name": "HTML + CSS + JavaScript",
"level": 5,
"visible": true,
"keywords": [],
"description": ""
},
{
"id": "c5qu0q3wct06oj1wa3u3tkar",
"visible": true,
"name": "Typescrpt",
"description": "",
"level": 5,
"keywords": []
},
{
"id": "qtq2qfeoa0bskykwhfzmlpng",
"visible": true,
"name": "Vitest, Jest, and Playwright",
"description": "",
"level": 4,
"keywords": []
},
{
"id": "b6k6q4r592uesacsz03dtvyk",
"visible": true,
"name": "Docker + Docker Compose",
"description": "",
"level": 5,
"keywords": []
},
{
"id": "lc3eu9r8vvqhsst1mkeqxgse",
"visible": true,
"name": "Go (Golang)",
"description": "",
"level": 4,
"keywords": []
},
{
"id": "lme3ob0kfpe5hgsuar42nmi6",
"visible": true,
"name": "SQL (PostgreSQL, MySQL, SQLite)",
"description": "",
"level": 4,
"keywords": []
},
{
"id": "f58rq48rtsgdftfcbpt785is",
"visible": true,
"name": "Python",
"description": "",
"level": 4,
"keywords": []
},
{
"id": "ht9fn1i89gm0e3gf5mfde0os",
"visible": true,
"name": "SCRUM",
"description": "",
"level": 5,
"keywords": []
},
{
"id": "vtpxeg6r0os9ygjmg384wo7f",
"visible": true,
"name": "Amazon Web Services (AWS)",
"description": "",
"level": 4,
"keywords": []
},
{
"id": "tk3i1xdw92vny0fk7001rrj7",
"visible": true,
"name": "Ruby",
"description": "",
"level": 2,
"keywords": []
},
{
"id": "jqy6vkxl8hed4vgow0z12vwy",
"visible": true,
"name": "Test Driven Development",
"description": "",
"level": 3,
"keywords": []
},
{
"id": "oalwcevey6plalwasugwf4q7",
"visible": true,
"name": "C#",
"description": "",
"level": 3,
"keywords": []
},
{
"id": "skhsek829sf8012wbwd38fl8",
"visible": true,
"name": "PHP",
"description": "",
"level": 2,
"keywords": []
},
{
"id": "feotadkdeli1ukx3u3ix86ig",
"name": "Time Management",
"level": 4,
"visible": true,
"keywords": [],
"description": ""
},
{
"id": "a993l06kuyinj9l88uz3ztux",
"name": "Problem Solving",
"level": 5,
"visible": true,
"keywords": [],
"description": ""
},
{
"id": "rhyu2toaznnidknrz244klqq",
"name": "Attention to Detail",
"level": 5,
"visible": true,
"keywords": [],
"description": ""
}
],
"columns": 1,
"visible": true,
"separateLinks": true
},
"summary": {
"id": "summary",
"name": "Summary",
"columns": 1,
"content": "<p>I am a full-stack web developer and researcher with a background maintaining and developing for large-scale enterprise software systems. I am in the process of completing my Master of Science in Computer Science under the supervision of Dr. Nathaniel Osgood at the University of Saskatchewan. I have completed my course work and am now moving into writing my thesis which can be done asynchronously.</p><p></p>",
"visible": true,
"separateLinks": true
},
"profiles": {
"id": "profiles",
"name": "Profiles",
"items": [
{
"id": "zuto1s9atwo6tdx9qfa9ggug",
"url": {
"href": "https://github.com/atridadl",
"label": ""
},
"icon": "github",
"network": "GitHub",
"visible": true,
"username": "atridadl"
},
{
"id": "satbehrw5da07dmi8y8j70kl",
"url": {
"href": "https://www.linkedin.com/in/atridadl/",
"label": ""
},
"icon": "linkedin",
"network": "linkedin",
"visible": true,
"username": "atridadl"
},
{
"id": "yorfn8ku98u5o0jzvumo9q2v",
"url": {
"href": "https://git.atri.dad/atridad",
"label": ""
},
"icon": "forgejo",
"network": "Forgejo",
"visible": true,
"username": "atridad"
}
],
"columns": 1,
"visible": true,
"separateLinks": true
},
"projects": {
"id": "projects",
"name": "Projects",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
},
"education": {
"id": "education",
"name": "Education",
"items": [
{
"id": "xtkfnu2zq3myh09pumehphx9",
"url": {
"href": "",
"label": ""
},
"area": "Computer Science",
"date": "2024 Present",
"score": "",
"summary": "<p style=\"text-align: left\">Supervisor: Dr. Nathaniel Osgood</p><ul><li><p style=\"text-align: left\">CMPT 838: Computer Security</p></li><li><p style=\"text-align: left\">CMPT 815: Computer Systems and Performance Evaluation</p></li></ul>",
"visible": true,
"studyType": "Masters",
"institution": "University of Saskatchewan"
},
{
"id": "o4my8au0d7c6bf09vlqwxvyw",
"url": {
"href": "",
"label": ""
},
"area": "Computer Science",
"date": "2017 2019",
"score": "",
"summary": "",
"visible": true,
"studyType": "Bachelors (3 Year)",
"institution": "University of Saskatchewan"
},
{
"id": "pnwpsei7ag1yldmtv9f4kt4e",
"url": {
"href": "",
"label": ""
},
"area": "Computer Engineering",
"date": "2012 2017",
"score": "",
"summary": "",
"visible": true,
"studyType": "Bachelors",
"institution": "University of Saskatchewan"
}
],
"columns": 1,
"visible": true,
"separateLinks": true
},
"interests": {
"id": "interests",
"name": "Interests",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
},
"languages": {
"id": "languages",
"name": "Languages",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
},
"volunteer": {
"id": "volunteer",
"name": "Volunteering",
"items": [
{
"id": "xhg1p7exqggrjkldszplj1wk",
"url": {
"href": "",
"label": ""
},
"date": "2021 2022",
"summary": "",
"visible": true,
"location": "",
"position": "Mentor",
"organization": "Big Brother Big Sisters"
}
],
"columns": 1,
"visible": true,
"separateLinks": true
},
"experience": {
"id": "experience",
"name": "Experience",
"items": [
{
"id": "gn67fi9oygi5tz1x3p3r7mbf",
"url": {
"href": "https://atash.dev",
"label": ""
},
"date": "June 2019 Present",
"company": "Atash Consulting",
"summary": "<ul><li><p>Builds mobile and web applications for small-medium sized businesses</p></li><li><p>Provides consulting on as application development, system architecture, DevOps, etc</p></li><li><p>Hosting websites for small-medium sized businesses</p></li></ul>",
"visible": true,
"location": "Edmonton, Alberta",
"position": "Owner/Developer"
},
{
"id": "x8ok2hutceh7lroyhwa7kj0h",
"url": {
"href": "",
"label": ""
},
"date": "November 2023 Present",
"company": "University of Saskatchewan CEPHIL Lab",
"summary": "<ul><li><p>Developing mobile and web applications</p></li><li><p>Coordinating with other grant researchers to deliver a minimum viable product</p></li><li><p>Gathering requirements from stakeholders to craft a product timeline</p></li><li><p>Acting as a technical lead and supervisor to a developer intern</p></li></ul>",
"visible": true,
"location": "Saskatoon, Saskatchewan",
"position": "Research Technician"
},
{
"id": "f0kyaxcy3syb8wazs3ye662i",
"url": {
"href": "",
"label": ""
},
"date": "August 2021 November 2023",
"company": "Alberta Motor Association",
"summary": "<ul><li><p>Developed and maintained internal enterprise-level business applications leveraging Amazon Web Services (AWS)</p></li><li><p>Used React and Create React App (CRA) for standalone applications and micro-front-ends</p></li><li><p>Developed an in-house payment gateway for all AMA services that integrates with Stripe</p></li><li><p>Provided tier 3 support support for internal service</p></li><li><p>Participated in a bi-monthly 24/7 on-call rotation</p></li><li><p>Mentored students in the organizations Developer in Training program</p></li></ul>",
"visible": true,
"location": "Edmonton, Alberta",
"position": "Software Developer II"
},
{
"id": "yikqef72i068lfiy8iiwjm45",
"url": {
"href": "",
"label": ""
},
"date": "October 2019 August 2021",
"company": "University of Alberta IST",
"summary": "<ul><li><p>Front-end development of web applications using Vue.js</p></li><li><p>Leveraged Amazon Web Services to adopt a serverless architecture</p></li><li><p>Maintained a secure exam application developed in-house</p></li><li><p>Monitored and maintained an exam scheduling system hosted on-premises</p></li></ul>",
"visible": true,
"location": "Edmonton, Alberta",
"position": "Software Developer"
},
{
"id": "wzqfv3h8rxs6574z5hlvrhm7",
"url": {
"href": "",
"label": ""
},
"date": "July 2017 October 2019",
"company": "University of Alberta IST",
"summary": "<ul><li><p>Provided support for our Moodle installation to students, faculty, and staff</p></li><li><p>Front-end development of web applications using Vue.js</p></li></ul>",
"visible": true,
"location": "Edmonton, Alberta",
"position": "Support Analyst"
}
],
"columns": 1,
"visible": true,
"separateLinks": true
},
"references": {
"id": "references",
"name": "References",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
},
"publications": {
"id": "publications",
"name": "Publications",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
},
"certifications": {
"id": "certifications",
"name": "Certifications",
"items": [],
"columns": 1,
"visible": true,
"separateLinks": true
}
}
}

View File

@ -1,37 +0,0 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "@tailwindcss/typography";
@plugin "daisyui/theme" {
name: "chaoticbisexual";
default: true;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(25.33% 0.016 252.42);
--color-base-200: oklch(23.26% 0.014 253.1);
--color-base-300: oklch(21.15% 0.012 254.09);
--color-base-content: oklch(97.807% 0.029 256.847);
--color-primary: oklch(65% 0.241 354.308);
--color-primary-content: oklch(96% 0.018 272.314);
--color-secondary: oklch(60% 0.25 292.717);
--color-secondary-content: oklch(94% 0.028 342.258);
--color-accent: oklch(78% 0.154 211.53);
--color-accent-content: oklch(38% 0.063 188.416);
--color-neutral: oklch(40% 0.17 325.612);
--color-neutral-content: oklch(92% 0.004 286.32);
--color-info: oklch(74% 0.16 232.661);
--color-info-content: oklch(29% 0.066 243.157);
--color-success: oklch(76% 0.177 163.223);
--color-success-content: oklch(37% 0.077 168.94);
--color-warning: oklch(82% 0.189 84.429);
--color-warning-content: oklch(41% 0.112 45.904);
--color-error: oklch(71% 0.194 13.428);
--color-error-content: oklch(27% 0.105 12.094);
--radius-selector: 1rem;
--radius-field: 1rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 1;
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": [
".astro/types.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.astro"
],
"exclude": [
"node_modules",
"dist"
]
}