15 Commits

Author SHA1 Message Date
2342d297ec feat: add copy-to-clipboard button to code blocks 2026-04-22 00:43:19 +02:00
fdc65cd870 better support code blocks 2026-04-22 00:34:59 +02:00
1e9509d449 fix: scope inline code styles to exclude code inside pre blocks 2026-04-22 00:24:29 +02:00
18fdbf1ce5 fix: replace rehype-pretty-code with react-syntax-highlighter for SSR compatibility 2026-04-22 00:14:29 +02:00
fb3718cf12 feat: add Shiki syntax highlighting to markdown via rehype-pretty-code 2026-04-22 00:12:03 +02:00
4e8975d947 fix: use bun to run server, node not available in bun image 2026-04-21 23:41:30 +02:00
62e73a4499 fix: use node-server preset for proper static asset serving 2026-04-21 23:38:03 +02:00
acc02d20a4 fix: single-stage bun Dockerfile to fix module compatibility 2026-04-21 23:34:05 +02:00
7151a0b7ba fix: bind server to 0.0.0.0 for container deployment 2026-04-21 23:33:21 +02:00
7146edc7bb fix: copy node_modules to runner stage 2026-04-21 23:27:47 +02:00
dcad455736 fix: correct build output path and guard devtools plugin in production 2026-04-21 23:25:05 +02:00
802e50378f fix: update Dockerfile for production deployment 2026-04-21 23:22:02 +02:00
58921ca65b chore: add Dockerfile for TanStack Start production build 2026-04-21 23:19:44 +02:00
2590d60e58 chore: remove Go/templ legacy implementation 2026-04-21 23:16:43 +02:00
4685507519 Merge pull request #2 from moh682/feat/strapi-integration
feat: integrate Strapi CMS for dynamic portfolio data
2026-04-21 23:11:06 +02:00
61 changed files with 255 additions and 4283 deletions

View File

@@ -1,18 +0,0 @@
{
"projectName": "portfolio",
"mode": "file-router",
"typescript": true,
"packageManager": "bun",
"includeExamples": false,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": false,
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"eslint",
"strapi"
]
}

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.output
.git
.env*
.DS_Store
*.log

View File

@@ -7,30 +7,41 @@ on:
workflow_dispatch:
permissions:
contents: write
packages: write
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v5
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
go-version: 1.22
cache: true
- uses: ko-build/setup-ko@v0.7
- uses: sigstore/cosign-installer@v3.7.0
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
cosign-release: v2.2.3
- uses: goreleaser/goreleaser-action@v5
with:
version: v1.24.0
args: release --clean
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
COSIGN_PASSWORD: '${{ secrets.COSIGN_PASSWORD }}'
COSIGN_PRIVATE_KEY: '${{ secrets.COSIGN_PRIVATE_KEY }}'
COSIGN_PUBLIC_KEY: '${{ secrets.COSIGN_PUBLIC_KEY }}'
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,16 +1,19 @@
# Build.
FROM golang:1.24.0 AS build-stage
FROM oven/bun:1.2
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . /app
RUN CGO_ENABLED=0 GOOS=linux go build -o ./entrypoint
# Deploy.
FROM gcr.io/distroless/static-debian11 AS release-stage
WORKDIR /
COPY --from=build-stage /app/entrypoint /entrypoint
COPY --from=build-stage /app/assets /assets
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/entrypoint"]
COPY package.json bun.lock ./
RUN bun install
COPY . .
ARG STRAPI_URL=https://strapi.moshariri.com
ENV STRAPI_URL=$STRAPI_URL
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
RUN bun run build
EXPOSE 3000
CMD ["bun", "run", ".output/server/index.mjs"]

View File

@@ -1,17 +0,0 @@
# run templ generation in watch mode to detect all .templ files and
# re-create _templ.txt files on change, then send reload event to browser.
# Default url: http://localhost:7331
live/templ:
templ generate --notify-proxy --proxybind="localhost" --proxyport="8080"
# run air to detect any go file changes to re-build and re-run the server.
live/server:
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
# run esbuild to generate the index.js bundle in watch mode.
live/rollup:
npm --prefix ts run build:watch
# start all 5 watch processes in parallel.
live:
make -j5 live/rollup live/server

BIN
assets/.DS_Store vendored

Binary file not shown.

View File

@@ -1,65 +0,0 @@
@layer base {
:root {
--navbar-height: 6rem;
--content-height: calc(100vh - var(--navbar-height));
}
main {
margin-top: var(--navbar-height);
height: 100%;
padding-bottom: 6rem;
overflow-x: hidden;
}
.navbar-drawer {
height: var(--content-height);
}
[x-cloak] {
display: none !important;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
opacity: 0;
animation: fadeIn 1s ease-in forwards;
}
.animate-slide-in {
opacity: 0;
animation: slideIn 1s ease-out forwards;
}
.delay-300 {
animation-delay: 300ms;
}
.delay-600 {
animation-delay: 600ms;
}
.delay-900 {
animation-delay: 900ms;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,2 +0,0 @@
!function(){"use strict";const{animate:i,inView:t}=Motion;t("section div",(t=>(i(t,{opacity:1},{duration:.7}),()=>i(t,{opacity:0},{duration:.7}))),{some:.7,initial:!1})}();
//# sourceMappingURL=experiences.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"experiences.js","sources":["../../ts/src/experiences.ts"],"sourcesContent":["const { animate, inView } = Motion;\n\ninView(\n \"section div\",\n (element) => {\n animate(\n element,\n { opacity: 1 },\n {\n duration: 0.7,\n },\n );\n return () => animate(element, { opacity: 0 }, { duration: 0.7 });\n },\n { some: 0.7, initial: false },\n);\n"],"names":["animate","inView","Motion","element","opacity","duration","some","initial"],"mappings":"yBAAA,MAAMA,QAAEA,EAAOC,OAAEA,GAAWC,OAE5BD,EACE,eACCE,IACCH,EACEG,EACA,CAAEC,QAAS,GACX,CACEC,SAAU,KAGP,IAAML,EAAQG,EAAS,CAAEC,QAAS,GAAK,CAAEC,SAAU,OAE5D,CAAEC,KAAM,GAAKC,SAAS"}

View File

@@ -1,2 +0,0 @@
!function(){"use strict";const{animate:e,stagger:n}=Motion,t=document.getElementById("years-experience");if(!t)throw new Error("years-experience Element not found");const o=document.getElementById("programming-languages");if(!o)throw new Error("languages Element not found");e(0,6,{duration:1.5,ease:"circOut",onUpdate:e=>{const n=Math.floor(e);t.innerHTML=String(n)}}),e(0,4,{duration:1.5,ease:"circOut",onUpdate:e=>{const n=Math.floor(e);o.innerHTML=String(n)}}),e("#years-experience",{count:5},{duration:1.5,delay:n(.1)}),e("#languages",{count:5},{duration:1.5,delay:n(.1)})}();
//# sourceMappingURL=landing.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"landing.js","sources":["../../ts/src/landing.ts"],"sourcesContent":["const { animate, stagger } = Motion;\n\nconst experienceCountSelector = \"#years-experience\";\nconst languagesCountSelector = \"#languages\";\n\nconst yrsExpCount = document.getElementById(\"years-experience\");\nif (!yrsExpCount) {\n throw new Error(\"years-experience Element not found\");\n}\n\nconst langCount = document.getElementById(\"programming-languages\");\nif (!langCount) {\n throw new Error(\"languages Element not found\");\n}\n\nanimate(0, 6, {\n duration: 1.5,\n ease: \"circOut\",\n onUpdate: (latest) => {\n const yrs = Math.floor(latest);\n yrsExpCount.innerHTML = String(yrs);\n },\n});\n\nanimate(0, 4, {\n duration: 1.5,\n ease: \"circOut\",\n onUpdate: (latest) => {\n const languages = Math.floor(latest);\n langCount.innerHTML = String(languages);\n },\n});\n\nanimate(\n experienceCountSelector,\n { count: 5 },\n { duration: 1.5, delay: stagger(0.1) },\n);\nanimate(\n languagesCountSelector,\n { count: 5 },\n { duration: 1.5, delay: stagger(0.1) },\n);\n"],"names":["animate","stagger","Motion","yrsExpCount","document","getElementById","Error","langCount","duration","ease","onUpdate","latest","yrs","Math","floor","innerHTML","String","languages","count","delay"],"mappings":"yBAAA,MAAMA,QAAEA,EAAOC,QAAEA,GAAYC,OAKvBC,EAAcC,SAASC,eAAe,oBAC5C,IAAKF,EACH,MAAM,IAAIG,MAAM,sCAGlB,MAAMC,EAAYH,SAASC,eAAe,yBAC1C,IAAKE,EACH,MAAM,IAAID,MAAM,+BAGlBN,EAAQ,EAAG,EAAG,CACZQ,SAAU,IACVC,KAAM,UACNC,SAAWC,IACT,MAAMC,EAAMC,KAAKC,MAAMH,GACvBR,EAAYY,UAAYC,OAAOJ,EAAI,IAIvCZ,EAAQ,EAAG,EAAG,CACZQ,SAAU,IACVC,KAAM,UACNC,SAAWC,IACT,MAAMM,EAAYJ,KAAKC,MAAMH,GAC7BJ,EAAUQ,UAAYC,OAAOC,EAAU,IAI3CjB,EA/BgC,oBAiC9B,CAAEkB,MAAO,GACT,CAAEV,SAAU,IAAKW,MAAOlB,EAAQ,MAElCD,EAnC+B,aAqC7B,CAAEkB,MAAO,GACT,CAAEV,SAAU,IAAKW,MAAOlB,EAAQ"}

View File

@@ -1,2 +0,0 @@
!function(){"use strict";const{animate:e,stagger:i}=Motion,t="#mobile-nav>li";document.addEventListener("alpine:init",(()=>{Alpine.data("navbar",(()=>({isOpen:!1,open(){this.isOpen=!0,e(t,{opacity:[0,1],x:[-50,0]},{delay:i(.1)})},toggle(){this.isOpen?this.close():this.open()},close(){setTimeout((()=>{this.isOpen=!1}),300),e(t,{opacity:[1,0],x:[0,50]},{delay:i(.1)})}})))}))}();
//# sourceMappingURL=navigation.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"navigation.js","sources":["../../ts/src/navigation.ts"],"sourcesContent":["const { animate, stagger } = Motion;\n\nconst navItemSelector = \"#mobile-nav>li\";\n\ndocument.addEventListener(\"alpine:init\", () => {\n Alpine.data(\"navbar\", () => {\n return {\n isOpen: false,\n open() {\n this.isOpen = true;\n animate(\n navItemSelector,\n { opacity: [0, 1], x: [-50, 0] },\n { delay: stagger(0.1) },\n );\n },\n toggle() {\n this.isOpen ? this.close() : this.open();\n },\n close() {\n setTimeout(() => {\n this.isOpen = false;\n }, 300);\n animate(\n navItemSelector,\n { opacity: [1, 0], x: [0, 50] },\n { delay: stagger(0.1) },\n );\n },\n };\n });\n});\n"],"names":["animate","stagger","Motion","navItemSelector","document","addEventListener","Alpine","data","isOpen","open","this","opacity","x","delay","toggle","close","setTimeout"],"mappings":"yBAAA,MAAMA,QAAEA,EAAOC,QAAEA,GAAYC,OAEvBC,EAAkB,iBAExBC,SAASC,iBAAiB,eAAe,KACvCC,OAAOC,KAAK,UAAU,KACb,CACLC,QAAQ,EACR,IAAAC,GACEC,KAAKF,QAAS,EACdR,EACEG,EACA,CAAEQ,QAAS,CAAC,EAAG,GAAIC,EAAG,EAAC,GAAK,IAC5B,CAAEC,MAAOZ,EAAQ,KAEpB,EACD,MAAAa,GACEJ,KAAKF,OAASE,KAAKK,QAAUL,KAAKD,MACnC,EACD,KAAAM,GACEC,YAAW,KACTN,KAAKF,QAAS,CAAK,GAClB,KACHR,EACEG,EACA,CAAEQ,QAAS,CAAC,EAAG,GAAIC,EAAG,CAAC,EAAG,KAC1B,CAAEC,MAAOZ,EAAQ,KAEpB,KAEH"}

View File

@@ -1,126 +0,0 @@
[
{
"start_date": "2022-07-01T00:00:00.000Z",
"end_date": null,
"company": {
"name": "Awaze",
"logo": "",
"link": "https://www.awaze.com/",
"location_link": "https://www.google.com/maps/place//data=!4m2!3m1!1s0x465252660a2125a1:0x5a2e60c905a1fd5a?sa=X&ved=1t:8290&ictx=111",
"city": "Virum (Copenhagen)",
"description": "Awaze is the largest managed vacation rentals and holiday resorts business in Europe, which brings together some of the continent's most trusted travel brands, including cottages.com, Hoseasons and NOVASOL."
},
"Job": {
"title": "Software Engineer",
"description": "I am working as a Software Engineer in the Awaze's Digital team. I am responsible for developing and maintaining the company's guest facing applications, that include Authentication, Booking Management and (FAQ) Help pages",
"objectives": ["Objective 1", "Objective 2", "Objective 3"],
"frontend_stack": [
{ "name": "NextJS", "link": "https://nextjs.org/" },
{ "name": "React", "link": "https://reactjs.org/" },
{ "name": "Zustand", "link": "https://zustand-demo.pmnd.rs/" },
{ "name": "Bit", "link": "https://bit.dev/" },
{
"name": "ApolloClient",
"link": "https://www.apollographql.com/docs/react"
},
{ "name": "Storybook", "link": "https://storybook.js.org/" },
{
"name": "Styled-Components",
"link": "https://styled-components.com/"
}
],
"backend_stack": [
{ "name": "Nodejs", "link": "https://nodejs.org/en" },
{ "name": "GraphQL", "link": "https://graphql.org/" },
{ "name": "Apollo", "link": "https://www.apollographql.com/docs/" },
{ "name": "Lambda", "link": "https://aws.amazon.com/lambda/" },
{ "name": "Traveller (MSSQL)", "link": "" }
],
"infrastructure_stack": [
{ "name": "Serverless", "link": "https://www.serverless.com/" },
{ "name": "Cognito", "link": "https://aws.amazon.com/cognito/" },
{ "name": "AWS SES", "link": "" },
{ "name": "Github", "link": "https://github.com/" }
]
}
},
{
"start_date": "2021-08-01T00:00:00.000Z",
"end_date": "2022-07-30T00:00:00.000Z",
"company": {
"name": "Kiapro",
"logo": "",
"link": "https://www.kiapro.dk/om-kiapro",
"location_link": "https://www.google.com/maps/place//data=!4m2!3m1!1s0x46525052d1d32a6d:0x47b68788842d92aa?sa=X&ved=1t:8290&ictx=111",
"city": "Herlev (Copenhagen)",
"description": "Kiapro is a Danish company that provides software solutions both for Fin-Tech and Healthcare sector. The company's main product is a platform that helps Fin-Tech management engage with employees through an interactive intranet.\nOther products include all-round applications for Chiropractors to follow patients through their transformation which is called BackTrace."
},
"Job": {
"title": "Full Stack Developer",
"description": "As a Full Stack Developer at Doctolib, I contributed to the development of the core booking platform and various healthcare professional tools, focusing on both performance optimization and feature development.",
"objectives": [
"Improved platform performance and scalability",
"Developed new features for healthcare professionals",
"Implemented real-time notification systems"
],
"frontend_stack": [
{ "name": "React", "link": "https://reactjs.org/" },
{ "name": "Redux", "link": "https://redux.js.org/" },
{ "name": "Capacitor", "link": "https://capacitorjs.com/" },
{ "name": "TypeScript", "link": "https://www.typescriptlang.org/" }
],
"backend_stack": [
{ "name": "PHP", "link": "https://www.ruby-lang.org/" },
{ "name": "PostgreSQL", "link": "https://www.postgresql.org/" },
{ "name": "Go", "link": "https://go.dev/" },
{ "name": "Fiber", "link": "https://gofiber.io/" },
{ "name": "Laravel", "link": "https://laravel.com/" }
],
"infrastructure_stack": [
{ "name": "Docker", "link": "https://www.docker.com/" },
{ "name": "Self-hosted VM", "link": "" }
]
}
},
{
"start_date": "2019-10-01T00:00:00.000Z",
"end_date": "2021-07-31T00:00:00.000Z",
"company": {
"name": "Fellowmind",
"logo": "",
"link": "https://www.fellowmind.com/da-dk/",
"location_link": "https://maps.google.com/maps?hl=da&gl=se&um=1&ie=UTF-8&fb=1&sa=X&ftid=0x465253083f4d1201:0x8e4ee256108ec86a",
"city": "Copenhagen Area",
"description": "Fellowmind helps customers with accelerating their digital transformation by using Microsoft's cloud-solutions, progress to agile development, implement integrated platforms and help end-users to apply those techonologies. With other words, to allow people to enjoy working with technology, and to make the technology work for them."
},
"Job": {
"title": "Software Engineer",
"description": "I was apart of the team called IntraActive which was responsible for creating Interactive Components that can be added to their pre-built Microsoft pages. I Developed and maintained a customisable Phonebook that can be configured to any company theme.",
"objectives": [
"Developed new monitoring integrations",
"Optimized data processing pipelines",
"Enhanced visualization components"
],
"frontend_stack": [
{
"name": "Microsoft SPFX",
"link": "https://learn.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview"
},
{
"name": "Microsoft Sharepoint",
"link": "https://learn.microsoft.com/en-us/sharepoint/"
},
{ "name": "TypeScript", "link": "https://www.typescriptlang.org/" },
{ "name": "React", "link": "https://reactjs.org/" },
{ "name": "Mobx", "link": "https://mobx.js.org/README.html" }
],
"backend_stack": [
{ "name": "C#", "link": "" },
{ "name": ".NET", "link": "" },
{ "name": "Azure Functions", "link": "" },
{ "name": "Cosmos", "link": "" }
],
"infrastructure_stack": [{ "name": "Microservices", "link": "" }]
}
}
]

View File

@@ -1,30 +0,0 @@
[
{
"name": "Invoice Management System",
"start": null,
"end": null,
"stack": [
"Go",
"Wails",
"React",
"SQLite3",
"TailwindCSS",
"MacOS",
"Windows"
],
"description": "Invoie Management System that helps mechanics create invoices, manage customers, finances and more.",
"thumbnail": "/assets/images/invoice-manager-thumbnail.png",
"github_link": "",
"status": "In Progress"
},
{
"name": "Portfolio",
"start": null,
"end": null,
"stack": ["Go", "Templ", "Motion", "TailwindCSS", "Typescript", "AlpineJS"],
"description": "The website you are currently on. A portfolio website that showcases my projects, experiences and skills.",
"thumbnail": "",
"github_link": "",
"status": "In Progress"
}
]

View File

@@ -17,6 +17,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^16.1.1",
"remark-gfm": "^4.0.0",
"tailwindcss": "^4.1.18",
"use-debounce": "^10.1.0",
@@ -30,8 +31,10 @@
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^28.1.0",
"nitro": "^3.0.260415-beta",
"prettier": "^3.8.1",
"typescript": "^5.7.2",
"vite": "^8.0.0",
@@ -436,10 +439,14 @@
"@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="],
@@ -596,12 +603,16 @@
"comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crossws": ["crossws@0.4.5", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
@@ -618,6 +629,8 @@
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
"db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
@@ -656,6 +669,8 @@
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"env-runner": ["env-runner@0.1.7", "", { "dependencies": { "crossws": "^0.4.4", "exsolve": "^1.0.8", "httpxy": "^0.5.0", "srvx": "^0.11.13" }, "peerDependencies": { "@netlify/runtime": "^4", "miniflare": "^4.20260317.3" }, "optionalPeers": ["@netlify/runtime", "miniflare"], "bin": { "env-runner": "dist/cli.mjs" } }, "sha512-i7h96jxETJYhXy5grgHNJ9xNzCzWIn9Ck/VkkYgOlE4gOqknsLX3CmlVb5LmwNex8sOoLFVZLz+TIw/+b5rktA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -712,6 +727,8 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -724,6 +741,8 @@
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -748,16 +767,28 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"h3": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="],
"h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
"hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
@@ -768,6 +799,8 @@
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"httpxy": ["httpxy@0.5.0", "", {}, "sha512-qwX7QX/rK2visT10/b7bSeZWQOMlSm3svTD0pZpU+vJjNUP0YHtNv4c3z+MO+MSnGuRFWJFdCZiV+7F7dXIOzg=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -854,6 +887,8 @@
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
"lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
"lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="],
@@ -964,6 +999,10 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"nf3": ["nf3@0.3.16", "", {}, "sha512-Gs0xRPpUm2nDkqbi40NJ9g7qDIcjcJzgExiydnq6LAyqhI2jfno8wG3NKTL+IiJsx799UHOb1CnSd4Wg4SG4Pw=="],
"nitro": ["nitro@3.0.260415-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.5", "db0": "^0.3.4", "env-runner": "^0.1.7", "h3": "^2.0.1-rc.20", "hookable": "^6.1.1", "nf3": "^0.3.16", "ocache": "^0.1.4", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.0-rc.15", "srvx": "^0.11.15", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.7" }, "peerDependencies": { "@vercel/queue": "^0.1.4", "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.60.1", "vite": "^7 || ^8", "xml2js": "^0.6.2", "zephyr-agent": "^0.2.0" }, "optionalPeers": ["@vercel/queue", "dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-J0ntJERWtIdvweZdmkCiF8eOFvP9fIAJR2gpeIDrHbAlYavK41WQfADo/YoZ/LF7RMTZBiPaH/pt2s/nPru9Iw=="],
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@@ -972,6 +1011,12 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"ocache": ["ocache@0.1.4", "", { "dependencies": { "ohash": "^2.0.11" } }, "sha512-e7geNdWjxSnvsSgvLuPvgKgu7ubM10ZmTPOgpr7mz2BXYtvjMKTiLhjFi/gWU8chkuP6hNkZBsa9LzOusyaqkQ=="],
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -1008,6 +1053,8 @@
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -1022,8 +1069,12 @@
"react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="],
"react-syntax-highlighter": ["react-syntax-highlighter@16.1.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
@@ -1146,6 +1197,8 @@
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
@@ -1162,6 +1215,8 @@
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
"unstorage": ["unstorage@2.0.0-alpha.7", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],

View File

@@ -1,202 +0,0 @@
package components
templ AboutPage() {
@rootLayout() {
<main class="w-full min-h-full py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto flex flex-col gap-16">
<!-- Hero Section -->
<section class="text-center flex flex-col items-center gap-6">
<div class="relative inline-block">
<img
src="/assets/images/profile.jpg"
alt="Mohammad Hariri"
class="w-40 h-40 rounded-full object-cover border-4 border-emerald-500 shadow-xl"
/>
<div class="absolute -bottom-2 left-1/2 -translate-x-1/2 bg-emerald-500 text-white px-3 py-1 rounded-full text-sm font-medium whitespace-nowrap">
Available for Work
</div>
</div>
<div>
<h1 class="text-4xl font-bold text-slate-100 mb-4">Your Name</h1>
<p class="text-xl text-slate-300">Full Stack Developer & Problem Solver</p>
</div>
<p class="text-slate-400 max-w-2xl mx-auto">
Passionate about crafting elegant solutions to complex problems. With expertise in modern web technologies,
I bring ideas to life through clean code and intuitive user experiences.
</p>
</section>
<!-- Journey Section -->
<section class="bg-slate-800/50 rounded-2xl p-8 backdrop-blur-sm">
<h2 class="text-2xl font-bold text-slate-100">My Journey</h2>
<div class="flex flex-col gap-8 mt-6">
<div class="flex gap-4">
<div class="flex-shrink-0 w-1 bg-emerald-500 rounded relative">
<div class="absolute w-3 h-3 bg-emerald-500 rounded-full -left-1 top-0"></div>
</div>
<div>
<h3 class="text-lg font-semibold text-emerald-400">Where I Started</h3>
<p class="text-slate-300 mt-2">
My journey in software development began with a curiosity about how things work.
From building simple websites to developing complex applications, every step has been a learning experience.
</p>
</div>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-1 bg-emerald-500 rounded relative">
<div class="absolute w-3 h-3 bg-emerald-500 rounded-full -left-1 top-0"></div>
</div>
<div>
<h3 class="text-lg font-semibold text-emerald-400">Where I Am</h3>
<p class="text-slate-300 mt-2">
Currently, I specialize in full-stack development, focusing on creating scalable and maintainable applications.
I'm passionate about using technology to solve real-world problems.
</p>
</div>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-1 bg-emerald-500 rounded relative">
<div class="absolute w-3 h-3 bg-emerald-500 rounded-full -left-1 top-0"></div>
</div>
<div>
<h3 class="text-lg font-semibold text-emerald-400">Where I'm Going</h3>
<p class="text-slate-300 mt-2">
My goal is to continue growing as a developer while contributing to meaningful projects that make a difference.
I'm always excited to learn new technologies and tackle new challenges.
</p>
</div>
</div>
</div>
</section>
<!-- Skills Section -->
<section class="flex flex-col gap-6">
<h2 class="text-2xl font-bold text-slate-100">Technical Expertise</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="bg-slate-800/50 p-6 rounded-xl backdrop-blur-sm">
<h3 class="text-lg font-semibold text-emerald-400">Frontend Development</h3>
<ul class="flex flex-col gap-2 text-slate-300 mt-4">
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
React & Next.js
</li>
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
TypeScript
</li>
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Tailwind CSS
</li>
</ul>
</div>
<div class="bg-slate-800/50 p-6 rounded-xl backdrop-blur-sm">
<h3 class="text-lg font-semibold text-emerald-400">Backend Development</h3>
<ul class="flex flex-col gap-2 text-slate-300 mt-4">
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Go & Fiber
</li>
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Node.js
</li>
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
PostgreSQL
</li>
</ul>
</div>
<div class="bg-slate-800/50 p-6 rounded-xl backdrop-blur-sm">
<h3 class="text-lg font-semibold text-emerald-400">Tools & Practices</h3>
<ul class="flex flex-col gap-2 text-slate-300 mt-4">
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Git & GitHub
</li>
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Docker
</li>
<li class="flex items-center">
<svg class="w-5 h-5 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
CI/CD
</li>
</ul>
</div>
</div>
</section>
<!-- Personal Section -->
<section class="bg-slate-800/50 rounded-2xl p-8 backdrop-blur-sm">
<h2 class="text-2xl font-bold text-slate-100 mb-6">Beyond the Code</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 class="text-lg font-semibold text-emerald-400">When I'm Not Coding</h3>
<p class="text-slate-300">
Outside of development, I enjoy [Your Hobbies/Interests]. I believe in maintaining a healthy work-life balance
and finding inspiration in various aspects of life.
</p>
</div>
<div>
<h3 class="text-lg font-semibold text-emerald-400">Let's Connect</h3>
<div class="flex flex-col gap-4">
<p class="text-slate-300">
I'm always interested in hearing about new projects and opportunities.
Feel free to reach out if you'd like to collaborate or just chat about technology.
</p>
<div class="flex gap-4">
<a
href="https://github.com/yourusername"
target="_blank"
rel="noopener noreferrer"
class="text-slate-400 hover:text-emerald-500 transition-colors"
>
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path>
</svg>
</a>
<a
href="https://linkedin.com/in/yourprofile"
target="_blank"
rel="noopener noreferrer"
class="text-slate-400 hover:text-emerald-500 transition-colors"
>
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>
</svg>
</a>
<a
href="https://twitter.com/yourhandle"
target="_blank"
rel="noopener noreferrer"
class="text-slate-400 hover:text-emerald-500 transition-colors"
>
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"></path>
</svg>
</a>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,109 +0,0 @@
package components
templ ContactPage() {
@rootLayout() {
<main class="w-full min-h-full py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-slate-100 mb-4">Get in Touch</h1>
<p class="text-slate-400">Have a question or want to work together? Let's connect!</p>
</div>
<div class="bg-slate-800/50 rounded-2xl p-8 backdrop-blur-sm shadow-xl">
<form id="contactForm" class="space-y-6" onsubmit="handleSubmit(event)">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-slate-300 mb-2">Name</label>
<input
type="text"
id="name"
name="name"
required
class="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent text-slate-100 placeholder-slate-400 transition-all"
placeholder="Your name"
/>
</div>
<div>
<label for="email" class="block text-sm font-medium text-slate-300 mb-2">Email</label>
<input
type="email"
id="email"
name="email"
required
class="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent text-slate-100 placeholder-slate-400 transition-all"
placeholder="your@email.com"
/>
</div>
</div>
<div>
<label for="subject" class="block text-sm font-medium text-slate-300 mb-2">Subject</label>
<input
type="text"
id="subject"
name="subject"
required
class="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent text-slate-100 placeholder-slate-400 transition-all"
placeholder="What's this about?"
/>
</div>
<div>
<label for="message" class="block text-sm font-medium text-slate-300 mb-2">Message</label>
<textarea
id="message"
name="message"
rows="6"
required
class="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent text-slate-100 placeholder-slate-400 transition-all resize-none"
placeholder="Your message here..."
></textarea>
</div>
<div class="flex items-center justify-between">
<button
type="submit"
class="inline-flex items-center px-6 py-3 border border-transparent rounded-lg text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 transition-all"
>
Send Message
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-2" viewBox="0 0 20 20" fill="currentColor">
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
</svg>
</button>
<div class="flex items-center space-x-4">
<a href="https://linkedin.com/in/yourprofile" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-slate-300 transition-colors">
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>
</svg>
</a>
<a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-slate-300 transition-colors">
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path>
</svg>
</a>
</div>
</div>
</form>
</div>
</div>
</main>
<script>
function handleSubmit(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
// Construct mailto URL
const email = 'your@email.com'; // Replace with your email
const subject = formData.get('subject');
const name = formData.get('name');
const message = `Name: ${name}\nEmail: ${formData.get('email')}\n\nMessage:\n${formData.get('message')}`;
const mailtoUrl = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`;
// Open email client
window.location.href = mailtoUrl;
// Optional: Reset form
form.reset();
}
</script>
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,65 +0,0 @@
package components
import "github.com/moh682/portfolio/domain"
import "slices"
import "strings"
templ experienceCard(exp domain.Experience) {
<section class="min-h-[var(--content-height)] w-full flex relative items-center">
<!-- Left Side: Job Details -->
<div class="p-6 flex flex-col gap-4 transition-all opacity-0">
<h3 class="text-xl font-semibold text-white">{ exp.Job.Title }</h3>
<div class="flex flex-col gap-2">
<a href={ templ.SafeURL(exp.Company.Link) } class="text-emerald-400 font-medium text-lg ">{ exp.Company.Name }</a>
<p class="text-slate-400 ">{ exp.Duration() }</p>
<p class="text-slate-600 text-sm">{ exp.GetStartDate() } - { exp.GetEndDate() } </p>
<a href={ templ.SafeURL(exp.Company.LocationLink) } class="text-emerald-500">{ exp.Company.City }</a>
</div>
<div class="flex flex-wrap gap-2">
for _, stack := range slices.Concat(exp.Job.FrontendStack, exp.Job.BackendStack, exp.Job.InfrastructureStack) {
@techStack(stack)
}
</div>
<div class="flex flex-col gap-2">
<h4 class="text-xl font-medium">About { exp.Company.Name }</h4>
for _, description := range strings.Split(exp.Company.Description, "\n") {
<p class="text-slate-300">
{ description }
</p>
}
</div>
<div class="flex flex-col gap-2">
<h4 class="text-xl font-medium">Role & Responsibilities</h4>
<div class="text-slate-300 whitespace-pre-wrap">
{ exp.Job.Description }
</div>
</div>
</div>
</section>
}
templ ExperiencesPage(experiences []domain.Experience) {
@rootLayout() {
<script defer src="assets/js/experiences.js"></script>
<main class="bg-gradient-to-b from-slate-900 w-full to-slate-800 px-6 overflow-x-hidden">
<div class="flex w-full items-center justify-between" x-data="{active:$persist(0)}">
<div class="relative w-full flex items-center justify-center">
<!-- Timeline -->
<div id="timeline" class="fixed w-10 top-[var(--navbar-height)] left-2 h-[var(--content-height)] flex items-center justify-center">
<div class="absolute bg-radial-[at_50%_25%] from-40% from-slate-700 h-full w-0.5 to-slate-800"></div>
<div id="timeline-point" class={ "flex items-center flex-grow-0 justify-center w-10 h-10 rounded-full bg-emerald-900 border-4 border-slate-800 z-[80]" }>
<div class="absolute w-2 h-2 z-[80] bg-emerald-400 rounded-full"></div>
</div>
</div>
<div id="exps-container" class="ml-4 items-center content-center relative xl:w-[1600px]">
for _, exp := range experiences {
@experienceCard(exp)
}
</div>
</div>
</div>
</main>
}
}

View File

@@ -1,280 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/moh682/portfolio/domain"
import "slices"
import "strings"
func experienceCard(exp domain.Experience) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"min-h-[var(--content-height)] w-full flex relative items-center\"><!-- Left Side: Job Details --><div class=\"p-6 flex flex-col gap-4 transition-all opacity-0\"><h3 class=\"text-xl font-semibold text-white\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(exp.Job.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 13, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex flex-col gap-2\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(exp.Company.Link)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"text-emerald-400 font-medium text-lg \">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(exp.Company.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 15, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</a><p class=\"text-slate-400 \">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(exp.Duration())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 16, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p><p class=\"text-slate-600 text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(exp.GetStartDate())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 17, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " - ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(exp.GetEndDate())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 17, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(exp.Company.LocationLink)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"text-emerald-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(exp.Company.City)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 18, Col: 99}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</a></div><div class=\"flex flex-wrap gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, stack := range slices.Concat(exp.Job.FrontendStack, exp.Job.BackendStack, exp.Job.InfrastructureStack) {
templ_7745c5c3_Err = techStack(stack).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div class=\"flex flex-col gap-2\"><h4 class=\"text-xl font-medium\">About ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(exp.Company.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 26, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, description := range strings.Split(exp.Company.Description, "\n") {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<p class=\"text-slate-300\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 29, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><div class=\"flex flex-col gap-2\"><h4 class=\"text-xl font-medium\">Role & Responsibilities</h4><div class=\"text-slate-300 whitespace-pre-wrap\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(exp.Job.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 36, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ExperiencesPage(experiences []domain.Experience) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<script defer src=\"assets/js/experiences.js\"></script> <main class=\"bg-gradient-to-b from-slate-900 w-full to-slate-800 px-6 overflow-x-hidden\"><div class=\"flex w-full items-center justify-between\" x-data=\"{active:$persist(0)}\"><div class=\"relative w-full flex items-center justify-center\"><!-- Timeline --><div id=\"timeline\" class=\"fixed w-10 top-[var(--navbar-height)] left-2 h-[var(--content-height)] flex items-center justify-center\"><div class=\"absolute bg-radial-[at_50%_25%] from-40% from-slate-700 h-full w-0.5 to-slate-800\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 = []any{"flex items-center flex-grow-0 justify-center w-10 h-10 rounded-full bg-emerald-900 border-4 border-slate-800 z-[80]"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div id=\"timeline-point\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/experiences_page.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"><div class=\"absolute w-2 h-2 z-[80] bg-emerald-400 rounded-full\"></div></div></div><div id=\"exps-container\" class=\"ml-4 items-center content-center relative xl:w-[1600px]\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, exp := range experiences {
templ_7745c5c3_Err = experienceCard(exp).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></div></div></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = rootLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,14 +0,0 @@
package components
var iconSize = "28"
templ externalLinks() {
<div class="fixed flex flex-col gap-6 right-5 bottom-0 p-5 w-5">
<a href="https://github.com/moh682" target="_blank" rel="noopener noreferrer">
<svg class="hover:cursor-pointer" xmlns="http://www.w3.org/2000/svg" width={ iconSize } height={ iconSize } viewBox="0 0 24 24" fill="transparent" stroke="var(--color-emerald-500)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>
</a>
<a href="https://www.linkedin.com/in/mohammad-hariri-337a0116b" target="_blank" rel="noopener noreferrer">
<svg class="hover:cursor-pointer" xmlns="http://www.w3.org/2000/svg" width={ iconSize } height={ iconSize } viewBox="0 0 {iconSize} {iconSize}" fill="none" stroke="var(--color-emerald-500)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect width="4" height="12" x="2" y="9"></rect><circle cx="4" cy="4" r="2"></circle></svg>
</a>
</div>
}

View File

@@ -1,94 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
var iconSize = "28"
func externalLinks() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"fixed flex flex-col gap-6 right-5 bottom-0 p-5 w-5\"><a href=\"https://github.com/moh682\" target=\"_blank\" rel=\"noopener noreferrer\"><svg class=\"hover:cursor-pointer\" xmlns=\"http://www.w3.org/2000/svg\" width=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(iconSize)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/external_links.templ`, Line: 8, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" height=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(iconSize)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/external_links.templ`, Line: 8, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" viewBox=\"0 0 24 24\" fill=\"transparent\" stroke=\"var(--color-emerald-500)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-github\"><path d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4\"></path><path d=\"M9 18c-4.51 2-5-2-7-2\"></path></svg></a> <a href=\"https://www.linkedin.com/in/mohammad-hariri-337a0116b\" target=\"_blank\" rel=\"noopener noreferrer\"><svg class=\"hover:cursor-pointer\" xmlns=\"http://www.w3.org/2000/svg\" width=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(iconSize)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/external_links.templ`, Line: 11, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" height=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(iconSize)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/external_links.templ`, Line: 11, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" viewBox=\"0 0 {iconSize} {iconSize}\" fill=\"none\" stroke=\"var(--color-emerald-500)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-linkedin\"><path d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z\"></path><rect width=\"4\" height=\"12\" x=\"2\" y=\"9\"></rect><circle cx=\"4\" cy=\"4\" r=\"2\"></circle></svg></a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,71 +0,0 @@
package components
templ LandingPage() {
@rootLayout() {
<script defer src="assets/js/landing.js"></script>
<main class="flex flex-col gap-8 bg-gradient-to-b from-slate-900 to-slate-800">
<!-- Hero Section -->
<div class="px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<div class="flex flex-col gap-24 ">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<!-- Left Content -->
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-2">
<span class="px-4 w-fit py-2 bg-emerald-900/50 text-emerald-400 rounded-full text-sm font-medium">Software Engineer</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white leading-tight">
Hey there! I'm Mohammad Hariri
</h1>
<p class="text-xl text-slate-400 leading-relaxed">
I turn complex problems into elegant code solutions. Passionate about building robust applications that make a real difference in people's lives.
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4">
<a href="#contact" class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-emerald-600 hover:bg-emerald-700 transition duration-300 shadow-lg hover:shadow-xl">
Let's get in touch
</a>
<a href="#portfolio" class="inline-flex items-center justify-center px-6 py-3 border-2 border-slate-700 text-base font-medium rounded-lg text-slate-300 hover:bg-slate-800 transition duration-300">
View My Work
</a>
</div>
<!-- Social Links -->
<div class="flex gap-6 pt-4">
<a href="https://github.com/moh682" class="text-slate-400 hover:text-emerald-400 transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"></path>
</svg>
</a>
<a href="https://www.linkedin.com/in/mohammad-hariri-337a0116b/" class="text-gray-600 hover:text-emerald-600 transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>
</svg>
</a>
</div>
</div>
<!-- Right Content - Hero Image -->
<div class="relative">
<div class="absolute -inset-4 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-2xl opacity-30 blur-xl"></div>
<div class="relative bg-slate-800 p-4 rounded-2xl shadow-xl">
<img
src="/assets/images/portfolio-photo.webp"
alt="Mohammad Hariri"
class="w-full h-full object-cover rounded-xl"
/>
</div>
</div>
</div>
<!-- Stats Section -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-8">
<div class="text-center">
<div id="years-experience" class="text-4xl font-bold text-emerald-400">0</div>
<div class="text-slate-400 mt-2">Years Experience</div>
</div>
<div class="text-center">
<div id="programming-languages" class="text-4xl font-bold text-emerald-600">0</div>
<div class="text-gray-600 mt-2">Programming Languages</div>
</div>
</div>
</div>
</div>
</main>
}
}

View File

@@ -1,58 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func LandingPage() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script defer src=\"assets/js/landing.js\"></script> <main class=\"flex flex-col gap-8 bg-gradient-to-b from-slate-900 to-slate-800\"><!-- Hero Section --><div class=\"px-4 sm:px-6 lg:px-8 py-16 sm:py-24\"><div class=\"flex flex-col gap-24 \"><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-12 items-center\"><!-- Left Content --><div class=\"flex flex-col gap-8\"><div class=\"flex flex-col gap-2\"><span class=\"px-4 w-fit py-2 bg-emerald-900/50 text-emerald-400 rounded-full text-sm font-medium\">Software Engineer</span><h1 class=\"text-4xl sm:text-5xl lg:text-6xl font-bold text-white leading-tight\">Hey there! I'm Mohammad Hariri</h1><p class=\"text-xl text-slate-400 leading-relaxed\">I turn complex problems into elegant code solutions. Passionate about building robust applications that make a real difference in people's lives.</p></div><div class=\"flex flex-col sm:flex-row gap-4\"><a href=\"#contact\" class=\"inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-emerald-600 hover:bg-emerald-700 transition duration-300 shadow-lg hover:shadow-xl\">Let's get in touch</a> <a href=\"#portfolio\" class=\"inline-flex items-center justify-center px-6 py-3 border-2 border-slate-700 text-base font-medium rounded-lg text-slate-300 hover:bg-slate-800 transition duration-300\">View My Work</a></div><!-- Social Links --><div class=\"flex gap-6 pt-4\"><a href=\"https://github.com/moh682\" class=\"text-slate-400 hover:text-emerald-400 transition-colors\"><svg class=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\"></path></svg></a> <a href=\"https://www.linkedin.com/in/mohammad-hariri-337a0116b/\" class=\"text-gray-600 hover:text-emerald-600 transition-colors\"><svg class=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z\"></path></svg></a></div></div><!-- Right Content - Hero Image --><div class=\"relative\"><div class=\"absolute -inset-4 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-2xl opacity-30 blur-xl\"></div><div class=\"relative bg-slate-800 p-4 rounded-2xl shadow-xl\"><img src=\"/assets/images/portfolio-photo.webp\" alt=\"Mohammad Hariri\" class=\"w-full h-full object-cover rounded-xl\"></div></div></div><!-- Stats Section --><div class=\"grid grid-cols-2 sm:grid-cols-4 gap-8\"><div class=\"text-center\"><div id=\"years-experience\" class=\"text-4xl font-bold text-emerald-400\">0</div><div class=\"text-slate-400 mt-2\">Years Experience</div></div><div class=\"text-center\"><div id=\"programming-languages\" class=\"text-4xl font-bold text-emerald-600\">0</div><div class=\"text-gray-600 mt-2\">Programming Languages</div></div></div></div></div></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = rootLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,23 +0,0 @@
package components
templ rootLayout() {
<html class="w-screen h-screen overflow-x-hidden bg-slate-900 text-white">
<head>
<!-- tailwindcss -->
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="assets/css/styles.css"/>
<!-- Motion.js -->
<script src="https://cdn.jsdelivr.net/npm/motion@latest/dist/motion.js"></script>
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Mohammad's Portfolio</title>
</head>
<body class="flex flex-col w-full h-full">
@navbar()
{ children... }
</body>
</html>
}

View File

@@ -1,52 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func rootLayout() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<html class=\"w-screen h-screen overflow-x-hidden bg-slate-900 text-white\"><head><!-- tailwindcss --><script src=\"https://unpkg.com/@tailwindcss/browser@4\"></script><link rel=\"stylesheet\" href=\"assets/css/styles.css\"><!-- Motion.js --><script src=\"https://cdn.jsdelivr.net/npm/motion@latest/dist/motion.js\"></script><!-- Alpine.js --><script defer src=\"https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js\"></script><script defer src=\"https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js\"></script><script defer src=\"https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js\"></script><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Mohammad's Portfolio</title></head><body class=\"flex flex-col w-full h-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = navbar().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,86 +0,0 @@
package components
templ navbar() {
<script src="/assets/js/navigation.js"></script>
<nav x-data="navbar" class="fixed h-[var(--navbar-height)] backdrop-blur-md bg-slate-900/90 z-50 w-full py-4 justify-between items-center flex gap-6 px-6 border-b border-slate-800">
<ul class="flex gap-4">
<li>
<a href="/" class="text-white hover:text-blue-400 transition-colors duration-300">
@Logo()
</a>
</li>
</ul>
<ul class="flex gap-4">
<div class="hidden md:block">
<div class="flex gap-8" id="desktop-nav">
<li class="relative group">
<a href="/experiences" class="flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z"></path>
</svg>
<span>Experiences</span>
</a>
</li>
<li class="relative group">
<a href="/projects" class="flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"></path>
</svg>
<span>Projects</span>
</a>
</li>
<li class="relative group">
<a href="/about" class="flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"></path>
</svg>
<span>About</span>
</a>
</li>
<li class="relative group">
<a href="/contact" class="flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"></path>
</svg>
<span>Contact</span>
</a>
</li>
</div>
</div>
</ul>
<ul class="w-6">
<button @click="toggle" class="md:hidden relative w-8 h-8 focus:outline-none">
<div class="absolute w-6 transform transition-all duration-300" :class="{'rotate-45 translate-y-0': isOpen, 'translate-y-[-8px]': !isOpen}">
<div class="h-[2px] w-full bg-white rounded-lg"></div>
</div>
<div class="absolute w-6 transform transition-all duration-300" :class="{'opacity-0': isOpen}">
<div class="h-[2px] w-full bg-white rounded-lg"></div>
</div>
<div class="absolute w-6 transform transition-all duration-300" :class="{'-rotate-45 translate-y-0': isOpen, 'translate-y-[8px]': !isOpen}">
<div class="h-[2px] w-full bg-white rounded-lg"></div>
</div>
</button>
<div
@click="close"
x-show="isOpen"
x-cloak
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="navbar-drawer top-[var(--navbar-height)] fixed inset-0 z-50 bg-slate-900/98 backdrop-blur-sm pt-24"
>
<div class="flex items-center justify-center h-full w-full">
<div id="mobile-nav" class="flex gap-12 flex-col items-center justify-center px-2">
<li><a class="text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300" href="/experiences">Experiences</a></li>
<li><a class="text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300" href="/projects">Projects</a></li>
<li><a class="text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300" href="/about">About</a></li>
<li><a class="text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300" href="/contact">Contact</a></li>
</div>
</div>
</div>
</ul>
</nav>
}

View File

@@ -1,48 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func navbar() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script src=\"/assets/js/navigation.js\"></script><nav x-data=\"navbar\" class=\"fixed h-[var(--navbar-height)] backdrop-blur-md bg-slate-900/90 z-50 w-full py-4 justify-between items-center flex gap-6 px-6 border-b border-slate-800\"><ul class=\"flex gap-4\"><li><a href=\"/\" class=\"text-white hover:text-blue-400 transition-colors duration-300\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Logo().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</a></li></ul><ul class=\"flex gap-4\"><div class=\"hidden md:block\"><div class=\"flex gap-8\" id=\"desktop-nav\"><li class=\"relative group\"><a href=\"/experiences\" class=\"flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300\"><svg class=\"w-5 h-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z\"></path></svg> <span>Experiences</span></a></li><li class=\"relative group\"><a href=\"/projects\" class=\"flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300\"><svg class=\"w-5 h-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5\"></path></svg> <span>Projects</span></a></li><li class=\"relative group\"><a href=\"/about\" class=\"flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300\"><svg class=\"w-5 h-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z\"></path></svg> <span>About</span></a></li><li class=\"relative group\"><a href=\"/contact\" class=\"flex items-center gap-2 text-slate-300 group-hover:text-white transition-colors duration-300\"><svg class=\"w-5 h-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75\"></path></svg> <span>Contact</span></a></li></div></div></ul><ul class=\"w-6\"><button @click=\"toggle\" class=\"md:hidden relative w-8 h-8 focus:outline-none\"><div class=\"absolute w-6 transform transition-all duration-300\" :class=\"{&#39;rotate-45 translate-y-0&#39;: isOpen, &#39;translate-y-[-8px]&#39;: !isOpen}\"><div class=\"h-[2px] w-full bg-white rounded-lg\"></div></div><div class=\"absolute w-6 transform transition-all duration-300\" :class=\"{&#39;opacity-0&#39;: isOpen}\"><div class=\"h-[2px] w-full bg-white rounded-lg\"></div></div><div class=\"absolute w-6 transform transition-all duration-300\" :class=\"{&#39;-rotate-45 translate-y-0&#39;: isOpen, &#39;translate-y-[8px]&#39;: !isOpen}\"><div class=\"h-[2px] w-full bg-white rounded-lg\"></div></div></button><div @click=\"close\" x-show=\"isOpen\" x-cloak x-transition:enter=\"transition ease-out duration-300\" x-transition:enter-start=\"opacity-0\" x-transition:enter-end=\"opacity-100\" x-transition:leave=\"transition ease-in duration-300\" x-transition:leave-start=\"opacity-100\" x-transition:leave-end=\"opacity-0\" class=\"navbar-drawer top-[var(--navbar-height)] fixed inset-0 z-50 bg-slate-900/98 backdrop-blur-sm pt-24\"><div class=\"flex items-center justify-center h-full w-full\"><div id=\"mobile-nav\" class=\"flex gap-12 flex-col items-center justify-center px-2\"><li><a class=\"text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300\" href=\"/experiences\">Experiences</a></li><li><a class=\"text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300\" href=\"/projects\">Projects</a></li><li><a class=\"text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300\" href=\"/about\">About</a></li><li><a class=\"text-4xl font-light text-slate-300 hover:text-white transition-colors duration-300\" href=\"/contact\">Contact</a></li></div></div></div></ul></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,10 +0,0 @@
package components
templ NotFound() {
@rootLayout() {
<main class="w-full h-full flex flex-col gap-0 items-center justify-center">
<h1 class="text-3xl">404</h1>
<p class="text-base">Page not found</p>
</main>
}
}

View File

@@ -1,58 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func NotFound() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<main class=\"w-full h-full flex flex-col gap-0 items-center justify-center\"><h1 class=\"text-3xl\">404</h1><p class=\"text-base\">Page not found</p></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = rootLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,65 +0,0 @@
package components
import "github.com/moh682/portfolio/domain"
templ tag(tag string) {
<span class="px-3 py-1 w-fit text-sm bg-slate-700 text-slate-300 rounded-full">{ tag }</span>
}
templ status(status string) {
<span class={ "px-3 py-1 w-fit text-sm bg-emerald-700 text-white rounded-full", templ.KV("bg-yellow-600/50", status =="In Progress") }>{ status }</span>
}
templ projectCard(project domain.Project) {
<div class="group bg-slate-800 rounded-lg overflow-hidden transition-all duration-300 hover:transform hover:scale-[1.02] hover:shadow-xl">
<div class="relative overflow-hidden aspect-video">
<img
src={ project.Thumbnail }
alt={ project.Name }
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div class="absolute inset-0 bg-gradient-to-t from-slate-900 to-transparent opacity-0 group-hover:opacity-60 transition-opacity duration-300"></div>
</div>
<div class="flex flex-col gap-4 p-6">
<div class="flex flex-col gap-2">
<h3 class="text-xl font-bold text-slate-100">{ project.Name }</h3>
@status(project.Status)
<p class="text-slate-400 mb-4 line-clamp-2">{ project.Description }</p>
<div class="flex flex-wrap gap-2">
for _, t := range project.Stack {
@tag(t)
}
</div>
</div>
if project.GithubLink != "" {
<a
href={ templ.SafeURL(project.GithubLink) }
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center text-emerald-500 hover:text-emerald-400 transition-colors"
>
View Project
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</a>
}
</div>
</div>
}
templ ProjectsPage(projects []domain.Project) {
@rootLayout() {
<main class="w-full min-h-full py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<h1 class="text-4xl font-bold text-slate-100 mb-2">Projects</h1>
<p class="text-slate-400 mb-12">A collection of projects I've worked on</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
for _, project := range projects {
@projectCard(project)
}
</div>
</div>
</main>
}
}

View File

@@ -1,290 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/moh682/portfolio/domain"
func tag(tag string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<span class=\"px-3 py-1 w-fit text-sm bg-slate-700 text-slate-300 rounded-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(tag)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 6, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func status(status string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var4 = []any{"px-3 py-1 w-fit text-sm bg-emerald-700 text-white rounded-full", templ.KV("bg-yellow-600/50", status == "In Progress")}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 10, Col: 144}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func projectCard(project domain.Project) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"group bg-slate-800 rounded-lg overflow-hidden transition-all duration-300 hover:transform hover:scale-[1.02] hover:shadow-xl\"><div class=\"relative overflow-hidden aspect-video\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(project.Thumbnail)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 17, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(project.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 18, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-110\"><div class=\"absolute inset-0 bg-gradient-to-t from-slate-900 to-transparent opacity-0 group-hover:opacity-60 transition-opacity duration-300\"></div></div><div class=\"flex flex-col gap-4 p-6\"><div class=\"flex flex-col gap-2\"><h3 class=\"text-xl font-bold text-slate-100\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(project.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 25, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = status(project.Status).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-slate-400 mb-4 line-clamp-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(project.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/projects_page.templ`, Line: 27, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p><div class=\"flex flex-wrap gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, t := range project.Stack {
templ_7745c5c3_Err = tag(t).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if project.GithubLink != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.SafeURL = templ.SafeURL(project.GithubLink)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var12)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"inline-flex items-center text-emerald-500 hover:text-emerald-400 transition-colors\">View Project <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z\" clip-rule=\"evenodd\"></path></svg></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ProjectsPage(projects []domain.Project) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<main class=\"w-full min-h-full py-16 px-4 sm:px-6 lg:px-8\"><div class=\"max-w-7xl mx-auto\"><h1 class=\"text-4xl font-bold text-slate-100 mb-2\">Projects</h1><p class=\"text-slate-400 mb-12\">A collection of projects I've worked on</p><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, project := range projects {
templ_7745c5c3_Err = projectCard(project).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></div></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = rootLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,24 +0,0 @@
package components
import "github.com/moh682/portfolio/domain"
templ techStack(stack domain.Stack) {
if stack.Link != "" {
<a
href={ templ.SafeURL(stack.Link) }
class="group flex items-center gap-2 px-3 py-2 rounded-md bg-slate-700/30 hover:bg-slate-700/50 transition-colors duration-200"
>
if stack.Logo != "" {
<img src={ stack.Logo } alt={ stack.Name + " logo" } class="w-5 h-5 object-contain"/>
}
<span class="text-emerald-500 w-fit font-medium group-hover:text-emerald-400">{ stack.Name }</span>
</a>
} else {
<div class="flex items-center gap-2 px-3 py-2 rounded-md bg-slate-700">
if stack.Logo != "" {
<img src={ stack.Logo } alt={ stack.Name + " logo" } class="w-5 h-5 object-contain"/>
}
<span class="text-white w-fit font-medium">{ stack.Name }</span>
</div>
}
}

View File

@@ -1,156 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/moh682/portfolio/domain"
func techStack(stack domain.Stack) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if stack.Link != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(stack.Link)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"group flex items-center gap-2 px-3 py-2 rounded-md bg-slate-700/30 hover:bg-slate-700/50 transition-colors duration-200\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if stack.Logo != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stack.Logo)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/tech_stack.templ`, Line: 12, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stack.Name + " logo")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/tech_stack.templ`, Line: 12, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"w-5 h-5 object-contain\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"text-emerald-500 w-fit font-medium group-hover:text-emerald-400\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stack.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/tech_stack.templ`, Line: 14, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"flex items-center gap-2 px-3 py-2 rounded-md bg-slate-700\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if stack.Logo != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(stack.Logo)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/tech_stack.templ`, Line: 19, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(stack.Name + " logo")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/tech_stack.templ`, Line: 19, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"w-5 h-5 object-contain\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"text-white w-fit font-medium\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(stack.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/tech_stack.templ`, Line: 21, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,34 +0,0 @@
services:
reverse-proxy:
image: traefik:v3.1
command:
- "--providers.docker"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=screenchok@gmail.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock
portfolio:
labels:
- "traefik.enable=true"
- "traefik.http.routers.guestbook.rule=Host(`moshariri.com`)"
- "traefik.http.routers.guestbook.entrypoints=websecure"
- "traefik.http.routers.guestbook.tls.certresolver=myresolver"
- "traefik.http.middlewares.forward-auth-headers.headers.customrequestheaders.X-Forwarded-For=true"
- "traefik.http.middlewares.forward-auth-headers.headers.customrequestheaders.X-Real-IP=true"
- "traefik.http.routers.guestbook.middlewares=forward-auth-headers"
- "com.centurylinklabs.watchtower.enable=true"
image: portfolio:prod
restart: always
volumes:
letsencrypt:

View File

@@ -1,83 +0,0 @@
package domain
import (
"fmt"
"time"
)
type Company struct {
Name string `json:"name"`
City string `json:"city"`
Link string `json:"link"`
Logo string `json:"logo"`
Description string `json:"description"`
LocationLink string `json:"location_link"`
}
type Stack struct {
Name string `json:"name"`
Logo string `json:"logo"`
Link string `json:"link"`
}
type Job struct {
Title string `json:"title"`
Description string `json:"description"`
Objectives []string `json:"objectives"`
FrontendStack []Stack `json:"frontend_stack"`
BackendStack []Stack `json:"backend_stack"`
InfrastructureStack []Stack `json:"infrastructure_stack"`
}
type Experience struct {
Company Company `json:"company"`
Job Job `json:"job"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
}
func (e Experience) Duration() string {
end := e.EndDate
if e.EndDate.IsZero() {
end = time.Now()
}
duration := end.Sub(e.StartDate)
years := int(duration.Hours() / (24 * 365))
months := int(duration.Hours()/(24*30)) - years*12
label := ""
if years == 1 {
label += "1 year"
}
if years > 1 {
label += fmt.Sprintf("%d years", years)
}
if label != "" {
label += ", "
}
if months == 1 {
label += "1 month"
}
if months > 1 {
label += fmt.Sprintf("%d months", months)
}
return label
}
func (e Experience) GetStartDate() string {
return e.StartDate.Format("Jan 2006")
}
func (e Experience) GetEndDate() string {
if e.EndDate.IsZero() {
return "Present"
}
return e.EndDate.Format("Jan 2006")
}

View File

@@ -1,14 +0,0 @@
package domain
import "time"
type Project struct {
Name string `json:"name"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Stack []string `json:"stack"`
Description string `json:"description"`
Thumbnail string `json:"thumbnail"`
GithubLink string `json:"github_link"`
Status string `json:"status"`
}

5
go.mod
View File

@@ -1,5 +0,0 @@
module github.com/moh682/portfolio
go 1.24.0
require github.com/a-h/templ v0.3.833

4
go.sum
View File

@@ -1,4 +0,0 @@
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

113
main.go
View File

@@ -1,113 +0,0 @@
package main
import (
_ "embed"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"github.com/a-h/templ"
"github.com/moh682/portfolio/components"
"github.com/moh682/portfolio/domain"
"github.com/moh682/portfolio/middlewares"
)
func templHandlerWithOptions(component templ.Component) http.Handler {
return templ.Handler(component, templ.WithStatus(200), templ.WithContentType("text/html"))
}
func rootHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r)
})
}
// SafeFileServer wraps http.FileServer to only serve specific file types from specific directories
func SafeFileServer(root http.FileSystem) http.Handler {
fileServer := http.FileServer(root)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
log.Println("assets handler", r.URL.Path)
log.Println(w.Header())
// Only allow specific file extensions
if allowed := func(path string) bool {
allowedExts := map[string]string{
".js": "text/javascript;charset=utf-8",
".css": "text/css;charset=utf-8",
".png": "image/png",
".webp": "image/webp",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
".ico": "image/x-icon",
}
for ext, contentType := range allowedExts {
if strings.HasSuffix(path, ext) {
w.Header().Set("Content-Type", contentType)
return true
}
}
return false
}(r.URL.Path); !allowed {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
fileServer.ServeHTTP(w, r)
})
}
//go:embed assets/json/experiences.json
var embeddedExperiences []byte
//go:embed assets/json/projects.json
var embeddedProjects []byte
func main() {
server := http.NewServeMux()
// Serve static files with restrictions
staticFS := http.Dir("assets")
server.Handle("GET /assets/", http.StripPrefix("/assets/", SafeFileServer(staticFS)))
server.Handle("GET /about", templHandlerWithOptions(components.AboutPage()))
var exps []domain.Experience
err := json.Unmarshal(embeddedExperiences, &exps)
if err != nil {
log.Panic(err)
}
server.Handle("GET /experiences", templHandlerWithOptions(components.ExperiencesPage(exps)))
var projects []domain.Project
err = json.Unmarshal(embeddedProjects, &projects)
if err != nil {
log.Panic(err)
}
server.Handle("GET /projects", templHandlerWithOptions(components.ProjectsPage(projects)))
server.Handle("GET /contact", templHandlerWithOptions(components.ContactPage()))
server.Handle("GET /{$}", templHandlerWithOptions(components.LandingPage()))
// 404 fallback page
server.Handle("GET /", templ.Handler(components.NotFound(), templ.WithStatus(404), templ.WithContentType("text/html")))
// TODO: add Blogs to my portfolio website
// server.Handle("GET /blogs", templHandlerWithOptions(components.AboutPage()))
fmt.Println("Listening on :8080")
log.Panic(
http.ListenAndServe(":8080",
middlewares.Combine(
middlewares.Logger(server),
),
),
)
}

View File

@@ -1,11 +0,0 @@
package middlewares
import "net/http"
func Combine(nexts ...http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, next := range nexts {
next.ServeHTTP(w, r)
}
})
}

View File

@@ -1,34 +0,0 @@
package middlewares
import (
"log"
"net/http"
"time"
)
type statusResponseWriter struct {
http.ResponseWriter
status int
}
func (w *statusResponseWriter) WriteHeader(code int) {
w.status = code
if w.status == 0 {
w.ResponseWriter.WriteHeader(code)
}
}
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ips := r.Header.Get("X-Forwarded-For")
rw := newStatusResponseWriter(w)
now := time.Now()
next.ServeHTTP(rw, r)
since := time.Since(now)
log.Println(r.Method, ips, r.URL.Path, rw.status, since)
})
}
func newStatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {
return &statusResponseWriter{ResponseWriter: w}
}

View File

@@ -8,6 +8,7 @@
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"start": "node .output/server/index.mjs",
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",
@@ -27,6 +28,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^16.1.1",
"remark-gfm": "^4.0.0",
"tailwindcss": "^4.1.18",
"use-debounce": "^10.1.0"
@@ -40,8 +42,10 @@
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^28.1.0",
"nitro": "^3.0.260415-beta",
"prettier": "^3.8.1",
"typescript": "^5.7.2",
"vite": "^8.0.0",

View File

@@ -1,6 +1,9 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { atomOneDark } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
import { fetchExperience } from '#/data/loaders/portfolio'
import { computeDuration, formatDate } from '#/data/portfolio'
import type { TExperience } from '#/types/strapi'
@@ -40,6 +43,79 @@ function TechChip({ label }: { readonly label: string }) {
)
}
// ── Code block renderer ───────────────────────────────────────────────────
function CodeBlock({
className,
children,
}: {
readonly className?: string
readonly children?: React.ReactNode
}) {
const [copied, setCopied] = useState(false)
const match = /language-(\w+)/.exec(className ?? '')
if (!match) {
return <code className={className}>{children}</code>
}
const handleCopy = () => {
void navigator.clipboard.writeText(String(children).replace(/\n$/, ''))
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div style={{ position: 'relative', margin: '1.5rem 0' }}>
<button
type="button"
onClick={handleCopy}
aria-label="Copy code"
style={{
position: 'absolute',
top: '0.6rem',
right: '0.6rem',
zIndex: 10,
background: 'var(--surface-strong)',
border: '1px solid var(--line)',
borderRadius: '0.4rem',
color: copied ? 'var(--lagoon)' : 'var(--sea-ink-soft)',
cursor: 'pointer',
fontSize: '0.7rem',
fontFamily: 'Manrope, sans-serif',
letterSpacing: '0.05em',
padding: '0.25rem 0.6rem',
transition: 'color 0.2s',
}}
>
{copied ? 'Copied!' : 'Copy'}
</button>
<SyntaxHighlighter
style={atomOneDark}
language={match[1]}
lineNumberStyle={{ color: 'var(--sea-ink-soft)' }}
wrapLongLines
showLineNumbers
customStyle={{
width: '100%',
background: 'var(--surface)',
border: '1px solid var(--line)',
borderRadius: '0.75rem',
fontSize: '0.875rem',
lineHeight: '1.7',
padding: '1.25rem 1.5rem',
}}
codeTagProps={{
style: { fontFamily: 'ui-monospace, monospace' },
}}
wrapLines
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
</div>
)
}
// ── Detail page ───────────────────────────────────────────────────────────
function ExperienceDetailPage() {
@@ -122,7 +198,10 @@ function ExperienceDetailPage() {
<section className="mt-10">
<div className="island-shell feature-card rounded-2xl p-6 sm:p-8">
<article className="prose prose-sm max-w-none prose-headings:font-bold prose-headings:text-[var(--sea-ink)] prose-p:text-[var(--sea-ink-soft)] prose-li:text-[var(--sea-ink-soft)] prose-strong:text-[var(--sea-ink)] prose-a:text-[var(--lagoon-deep)]">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{ code: CodeBlock }}
>
{exp.body}
</ReactMarkdown>
</article>

View File

@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Manrope:wght@400;500;600;700;800&display=swap");
@import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Manrope:wght@400;500;600;700;800&display=swap');
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@theme {
--font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif;
--font-sans: 'Manrope', ui-sans-serif, system-ui, sans-serif;
}
:root {
@@ -28,7 +28,7 @@
--hero-b: rgba(47, 106, 74, 0.2);
}
:root[data-theme="dark"] {
:root[data-theme='dark'] {
--sea-ink: #d7ece8;
--sea-ink-soft: #afcdc8;
--lagoon: #60d7cf;
@@ -51,7 +51,7 @@
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
:root:not([data-theme='light']) {
--sea-ink: #d7ece8;
--sea-ink-soft: #afcdc8;
--lagoon: #60d7cf;
@@ -92,28 +92,45 @@ body {
background:
radial-gradient(1100px 620px at -8% -10%, var(--hero-a), transparent 58%),
radial-gradient(1050px 620px at 112% -12%, var(--hero-b), transparent 62%),
radial-gradient(720px 380px at 50% 115%, rgba(79, 184, 178, 0.1), transparent 68%),
linear-gradient(180deg, color-mix(in oklab, var(--sand) 68%, white) 0%, var(--foam) 44%, var(--bg-base) 100%);
radial-gradient(
720px 380px at 50% 115%,
rgba(79, 184, 178, 0.1),
transparent 68%
),
linear-gradient(
180deg,
color-mix(in oklab, var(--sand) 68%, white) 0%,
var(--foam) 44%,
var(--bg-base) 100%
);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body::before {
content: "";
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
opacity: 0.28;
background:
radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.8), transparent 34%),
radial-gradient(circle at 78% 26%, rgba(79, 184, 178, 0.2), transparent 42%),
radial-gradient(
circle at 20% 15%,
rgba(255, 255, 255, 0.8),
transparent 34%
),
radial-gradient(
circle at 78% 26%,
rgba(79, 184, 178, 0.2),
transparent 42%
),
radial-gradient(circle at 42% 82%, rgba(47, 106, 74, 0.14), transparent 36%);
}
body::after {
content: "";
content: '';
position: fixed;
inset: 0;
pointer-events: none;
@@ -160,7 +177,7 @@ pre code {
}
.display-title {
font-family: "Fraunces", Georgia, serif;
font-family: 'Fraunces', Georgia, serif;
}
.island-shell {
@@ -174,7 +191,11 @@ pre code {
}
.feature-card {
background: linear-gradient(165deg, color-mix(in oklab, var(--surface-strong) 93%, white 7%), var(--surface));
background: linear-gradient(
165deg,
color-mix(in oklab, var(--surface-strong) 93%, white 7%),
var(--surface)
);
box-shadow:
0 1px 0 var(--inset-glint) inset,
0 18px 34px rgba(30, 90, 72, 0.1),
@@ -189,7 +210,10 @@ pre code {
button,
.island-shell,
a {
transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease,
transition:
background-color 180ms ease,
color 180ms ease,
border-color 180ms ease,
transform 180ms ease;
}
@@ -210,7 +234,7 @@ a {
}
.nav-link::after {
content: "";
content: '';
position: absolute;
left: 0;
bottom: -6px;
@@ -257,3 +281,13 @@ a {
transform: translateY(0);
}
}
/* ── Inline code ──────────────────────────────────────────────────────── */
.prose :not(pre) > code:not([class]) {
background: var(--chip-bg);
border: 1px solid var(--chip-line);
border-radius: 0.3rem;
padding: 0.15em 0.45em;
font-size: 0.85em;
font-family: ui-monospace, monospace;
}

1521
ts/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
{
"name": "ts",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "rollup -c",
"build:watch": "rollup -c -w",
"dev": "vite"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@types/alpinejs": "^3.13.11",
"vite": "^6.1.1"
},
"dependencies": {
"motion": "^12.4.7",
"motion-dom": "^12.4.5"
}
}

View File

@@ -1,40 +0,0 @@
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import terser from "@rollup/plugin-terser";
const parseConfig = (input) => ({
input: `src/${input}`,
output: {
file: `../assets/js/${input.slice(0, input.length - 3)}.js`,
format: "iife", // Changed from 'cjs' to 'iife' for browser use
name: "navigation",
extend: true,
sourcemap: true,
},
plugins: [
resolve({
browser: true,
preferBuiltins: false,
}),
commonjs({
include: /node_modules/,
transformMixedEsModules: true,
}),
typescript({
include: ["./src/types.d.ts"],
tsconfig: "./tsconfig.json",
}),
terser({
format: {
comments: false,
},
}),
],
});
export default [
parseConfig("navigation.ts"),
parseConfig("landing.ts"),
parseConfig("experiences.ts"),
];

View File

@@ -1,16 +0,0 @@
const { animate, inView } = Motion;
inView(
"section div",
(element) => {
animate(
element,
{ opacity: 1 },
{
duration: 0.7,
},
);
return () => animate(element, { opacity: 0 }, { duration: 0.7 });
},
{ some: 0.7, initial: false },
);

View File

@@ -1,43 +0,0 @@
const { animate, stagger } = Motion;
const experienceCountSelector = "#years-experience";
const languagesCountSelector = "#languages";
const yrsExpCount = document.getElementById("years-experience");
if (!yrsExpCount) {
throw new Error("years-experience Element not found");
}
const langCount = document.getElementById("programming-languages");
if (!langCount) {
throw new Error("languages Element not found");
}
animate(0, 6, {
duration: 1.5,
ease: "circOut",
onUpdate: (latest) => {
const yrs = Math.floor(latest);
yrsExpCount.innerHTML = String(yrs);
},
});
animate(0, 4, {
duration: 1.5,
ease: "circOut",
onUpdate: (latest) => {
const languages = Math.floor(latest);
langCount.innerHTML = String(languages);
},
});
animate(
experienceCountSelector,
{ count: 5 },
{ duration: 1.5, delay: stagger(0.1) },
);
animate(
languagesCountSelector,
{ count: 5 },
{ duration: 1.5, delay: stagger(0.1) },
);

View File

@@ -1,32 +0,0 @@
const { animate, stagger } = Motion;
const navItemSelector = "#mobile-nav>li";
document.addEventListener("alpine:init", () => {
Alpine.data("navbar", () => {
return {
isOpen: false,
open() {
this.isOpen = true;
animate(
navItemSelector,
{ opacity: [0, 1], x: [-50, 0] },
{ delay: stagger(0.1) },
);
},
toggle() {
this.isOpen ? this.close() : this.open();
},
close() {
setTimeout(() => {
this.isOpen = false;
}, 300);
animate(
navItemSelector,
{ opacity: [1, 0], x: [0, 50] },
{ delay: stagger(0.1) },
);
},
};
});
});

10
ts/src/types.d.ts vendored
View File

@@ -1,10 +0,0 @@
import type motionPkg from "motion";
import type alpine from "alpinejs";
declare global {
interface Window {
Motion: typeof motionPkg;
}
const Motion: typeof motionPkg;
const Alpine: typeof alpine;
}

View File

@@ -1,113 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
"moduleDetection": "force" /* Control what method is used to detect module-format JS files. */,
/* Modules */
"module": "esnext" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. *
"types": [
"./src/types.d.ts"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@@ -1,14 +1,25 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { nitro } from 'nitro/vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
const isProd = process.env['NODE_ENV'] === 'production'
const config = defineConfig({
resolve: { tsconfigPaths: true },
plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()],
server: {
host: '0.0.0.0',
port: 3000,
},
plugins: [
!isProd && devtools(),
tailwindcss(),
tanstackStart(),
nitro({ preset: 'node-server' }),
viteReact(),
].filter(Boolean),
})
export default config