Got the homepage sorted
This commit is contained in:
35
.github/workflows/deploy.yml
vendored
35
.github/workflows/deploy.yml
vendored
@ -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
31
.gitignore
vendored
@ -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
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"deno.enable": true
|
||||
}
|
35
Dockerfile
35
Dockerfile
@ -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"]
|
49
README.md
49
README.md
@ -1,3 +1,48 @@
|
||||
# Personal Site
|
||||
# Astro Starter Kit: Basics
|
||||
|
||||
Re-written with Deno + Fresh :)
|
||||
```sh
|
||||
pnpm create astro@latest -- --template basics
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||

|
||||
|
||||
## 🚀 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
23
astro.config.mjs
Normal 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'
|
||||
})
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
32
deno.json
32
deno.json
@ -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
8
dev.ts
@ -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);
|
@ -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"
|
@ -1,6 +0,0 @@
|
||||
import { defineConfig } from "$fresh/server.ts";
|
||||
import tailwind from "@pakornv/fresh-plugin-tailwindcss";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwind()],
|
||||
});
|
43
fresh.gen.ts
43
fresh.gen.ts
@ -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;
|
161
islands/Chat.tsx
161
islands/Chat.tsx
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
45
lib/posts.ts
45
lib/posts.ts
@ -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
13
main.ts
@ -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
27
package.json
Normal 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
4881
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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.
|
@ -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.
|
@ -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).
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
import { FreshContext } from "$fresh/server.ts";
|
||||
|
||||
export const handler = (_req: Request, _ctx: FreshContext): Response => {
|
||||
return new Response("pong");
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
41
src/components/SocialLinks.astro
Normal file
41
src/components/SocialLinks.astro
Normal 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>
|
74
src/components/TechLinks.astro
Normal file
74
src/components/TechLinks.astro
Normal 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
26
src/layouts/Layout.astro
Normal 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>
|
@ -1,10 +1,11 @@
|
||||
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 (
|
||||
<>
|
||||
<Layout>
|
||||
<img
|
||||
src="/logo.webp"
|
||||
alt="A drawing of Atridad Lahiji by Shelze!"
|
||||
@ -28,7 +29,5 @@ export default function Home() {
|
||||
|
||||
<TechLinks />
|
||||
|
||||
<HomeButtonLinks />
|
||||
</>
|
||||
);
|
||||
}
|
||||
<!-- <HomeButtonLinks /> -->
|
||||
</Layout>
|
36
src/styles/global.css
Normal file
36
src/styles/global.css
Normal 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;
|
||||
}
|
Binary file not shown.
@ -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 organization’s 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
17
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user