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
|
# build output
|
||||||
.env
|
dist/
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# Fresh build directory
|
# generated types
|
||||||
_fresh/
|
.astro/
|
||||||
# npm dependencies
|
|
||||||
|
# dependencies
|
||||||
node_modules/
|
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 { useComputed, useSignal } from "@preact/signals";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import {
|
import { Home, NotebookPen, FileText, CodeXml, MessageCircle } from 'lucide-preact';
|
||||||
LuCodeXml,
|
|
||||||
LuHouse,
|
|
||||||
LuMessageCircle,
|
|
||||||
LuNotebookPen,
|
|
||||||
LuFileText,
|
|
||||||
} from "@preact-icons/lu";
|
|
||||||
|
|
||||||
interface NavigationBarProps {
|
interface NavigationBarProps {
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
@ -27,7 +21,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let scrollTimer: number | undefined;
|
let scrollTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
isScrolling.value = true;
|
isScrolling.value = true;
|
||||||
@ -59,7 +53,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
<li class="mx-1">
|
<li class="mx-1">
|
||||||
<a href="/" class={currentPath === "/" ? "menu-active" : ""}>
|
<a href="/" class={currentPath === "/" ? "menu-active" : ""}>
|
||||||
<div class="tooltip" data-tip="Home">
|
<div class="tooltip" data-tip="Home">
|
||||||
<LuHouse class="text-xl" />
|
<Home />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -70,7 +64,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
class={isPostsPath(currentPath) ? "menu-active" : ""}
|
class={isPostsPath(currentPath) ? "menu-active" : ""}
|
||||||
>
|
>
|
||||||
<div class="tooltip" data-tip="Posts">
|
<div class="tooltip" data-tip="Posts">
|
||||||
<LuNotebookPen class="text-xl" />
|
<NotebookPen />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -81,7 +75,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
class={currentPath === "/resume" ? "menu-active" : ""}
|
class={currentPath === "/resume" ? "menu-active" : ""}
|
||||||
>
|
>
|
||||||
<div class="tooltip" data-tip="Resume">
|
<div class="tooltip" data-tip="Resume">
|
||||||
<LuFileText class="text-xl" />
|
<FileText />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -92,7 +86,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
class={currentPath.startsWith("/projects") ? "menu-active" : ""}
|
class={currentPath.startsWith("/projects") ? "menu-active" : ""}
|
||||||
>
|
>
|
||||||
<div class="tooltip" data-tip="Projects">
|
<div class="tooltip" data-tip="Projects">
|
||||||
<LuCodeXml class="text-xl" />
|
<CodeXml />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -103,7 +97,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|||||||
class={currentPath.startsWith("/chat") ? "menu-active" : ""}
|
class={currentPath.startsWith("/chat") ? "menu-active" : ""}
|
||||||
>
|
>
|
||||||
<div class="tooltip" data-tip="Chat">
|
<div class="tooltip" data-tip="Chat">
|
||||||
<LuMessageCircle class="text-xl" />
|
<MessageCircle />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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,11 +1,12 @@
|
|||||||
import HomeButtonLinks from "../components/HomeButtonLinks.tsx";
|
---
|
||||||
import SocialLinks from "../components/SocialLinks.tsx";
|
import SocialLinks from '../components/SocialLinks.astro';
|
||||||
import TechLinks from "../components/TechLinks.tsx";
|
import TechLinks from '../components/TechLinks.astro';
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
import '../styles/global.css';
|
||||||
|
---
|
||||||
|
|
||||||
export default function Home() {
|
<Layout>
|
||||||
return (
|
<img
|
||||||
<>
|
|
||||||
<img
|
|
||||||
src="/logo.webp"
|
src="/logo.webp"
|
||||||
alt="A drawing of Atridad Lahiji by Shelze!"
|
alt="A drawing of Atridad Lahiji by Shelze!"
|
||||||
height={150}
|
height={150}
|
||||||
@ -28,7 +29,5 @@ export default function Home() {
|
|||||||
|
|
||||||
<TechLinks />
|
<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