Compare commits
20 Commits
feat/react
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2342d297ec | |||
| fdc65cd870 | |||
| 1e9509d449 | |||
| 18fdbf1ce5 | |||
| fb3718cf12 | |||
| 4e8975d947 | |||
| 62e73a4499 | |||
| acc02d20a4 | |||
| 7151a0b7ba | |||
| 7146edc7bb | |||
| dcad455736 | |||
| 802e50378f | |||
| 58921ca65b | |||
| 2590d60e58 | |||
| 4685507519 | |||
| efadaa6918 | |||
| 7285dfd74d | |||
| ef9f16c2cc | |||
| 67e01e9a5e | |||
| 06e0d022c2 |
18
.cta.json
18
.cta.json
@@ -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
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.output
|
||||
.git
|
||||
.env*
|
||||
.DS_Store
|
||||
*.log
|
||||
47
.github/workflows/release.yml
vendored
47
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
31
Dockerfile
31
Dockerfile
@@ -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"]
|
||||
|
||||
17
Makefile
17
Makefile
@@ -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
BIN
assets/.DS_Store
vendored
Binary file not shown.
@@ -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 |
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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": "" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
55
bun.lock
55
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
}
|
||||
@@ -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=\"{'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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
5
go.mod
@@ -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
4
go.sum
@@ -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
113
main.go
@@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
67
src/data/loaders/portfolio.ts
Normal file
67
src/data/loaders/portfolio.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import type { TExperience, TEducation, TProfile } from '#/types/strapi'
|
||||
|
||||
const STRAPI_BASE = process.env['STRAPI_URL'] ?? 'https://strapi.moshariri.com'
|
||||
|
||||
const buildHeaders = (): Record<string, string> => {
|
||||
const token = process.env['STRAPI_API_TOKEN']
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
export const fetchExperiences = createServerFn({ method: 'GET' }).handler(
|
||||
async (): Promise<TExperience[]> => {
|
||||
const url = `${STRAPI_BASE}/api/experiences?populate=*&populate[objectives]=*&populate[technologies]=*`
|
||||
console.log(`[Strapi] → GET ${url}`)
|
||||
const res = await fetch(url, { headers: buildHeaders() })
|
||||
if (!res.ok)
|
||||
throw new Error(`Strapi request failed: ${res.status} ${res.url}`)
|
||||
const json = (await res.json()) as { data: TExperience[] }
|
||||
console.log(`[Strapi] ← GET ${url} (${json.data.length} items)`)
|
||||
return json.data.map((item) => ({
|
||||
...item,
|
||||
objectives: item.objectives ?? [],
|
||||
technologies: item.technologies ?? [],
|
||||
}))
|
||||
},
|
||||
)
|
||||
|
||||
export const fetchExperience = createServerFn({ method: 'GET' })
|
||||
.inputValidator((slug: string) => slug)
|
||||
.handler(async ({ data: slug }): Promise<TExperience> => {
|
||||
const url = `${STRAPI_BASE}/api/experiences?filters[slug][$eq]=${slug}&populate=*&populate[objectives]=*&populate[technologies]=*`
|
||||
console.log(`[Strapi] → GET ${url}`)
|
||||
const res = await fetch(url, { headers: buildHeaders() })
|
||||
if (!res.ok)
|
||||
throw new Error(`Strapi request failed: ${res.status} ${res.url}`)
|
||||
const json = (await res.json()) as { data: TExperience[] }
|
||||
const item = json.data[0]
|
||||
if (!item) throw new Error(`Experience not found: ${slug}`)
|
||||
console.log(`[Strapi] ← GET ${url} OK (${item.company})`)
|
||||
return item
|
||||
})
|
||||
|
||||
export const fetchEducations = createServerFn({ method: 'GET' }).handler(
|
||||
async (): Promise<TEducation[]> => {
|
||||
const url = `${STRAPI_BASE}/api/educations?populate=*`
|
||||
console.log(`[Strapi] → GET ${url}`)
|
||||
const res = await fetch(url, { headers: buildHeaders() })
|
||||
if (!res.ok)
|
||||
throw new Error(`Strapi request failed: ${res.status} ${res.url}`)
|
||||
const json = (await res.json()) as { data: TEducation[] }
|
||||
console.log(`[Strapi] ← GET ${url} (${json.data.length} items)`)
|
||||
return json.data
|
||||
},
|
||||
)
|
||||
|
||||
export const fetchProfile = createServerFn({ method: 'GET' }).handler(
|
||||
async (): Promise<TProfile> => {
|
||||
const url = `${STRAPI_BASE}/api/profile?populate[skills][populate]=*`
|
||||
console.log(`[Strapi] → GET ${url}`)
|
||||
const res = await fetch(url, { headers: buildHeaders() })
|
||||
if (!res.ok)
|
||||
throw new Error(`Strapi request failed: ${res.status} ${res.url}`)
|
||||
const json = (await res.json()) as { data: TProfile }
|
||||
console.log(`[Strapi] ← GET ${url} OK`)
|
||||
return json.data
|
||||
},
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { strapi } from '@strapi/client'
|
||||
|
||||
// Strapi base URL (without /api)
|
||||
// Strapi base URL (without /api) — client-safe env var only
|
||||
const STRAPI_BASE = import.meta.env.VITE_STRAPI_URL ?? 'http://localhost:1337'
|
||||
|
||||
// Initialize the Strapi SDK with /api endpoint
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as ExperienceSlugRouteImport } from './routes/experience/$slug'
|
||||
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
@@ -22,31 +23,40 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ExperienceSlugRoute = ExperienceSlugRouteImport.update({
|
||||
id: '/experience/$slug',
|
||||
path: '/experience/$slug',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/experience/$slug': typeof ExperienceSlugRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/experience/$slug': typeof ExperienceSlugRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/experience/$slug': typeof ExperienceSlugRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/about'
|
||||
fullPaths: '/' | '/about' | '/experience/$slug'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/about'
|
||||
id: '__root__' | '/' | '/about'
|
||||
to: '/' | '/about' | '/experience/$slug'
|
||||
id: '__root__' | '/' | '/about' | '/experience/$slug'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
ExperienceSlugRoute: typeof ExperienceSlugRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -65,12 +75,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/experience/$slug': {
|
||||
id: '/experience/$slug'
|
||||
path: '/experience/$slug'
|
||||
fullPath: '/experience/$slug'
|
||||
preLoaderRoute: typeof ExperienceSlugRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
ExperienceSlugRoute: ExperienceSlugRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
213
src/routes/experience/$slug.tsx
Normal file
213
src/routes/experience/$slug.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
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'
|
||||
|
||||
export const Route = createFileRoute('/experience/$slug')({
|
||||
loader: ({ params }) => fetchExperience({ data: params.slug }),
|
||||
errorComponent: ExperienceErrorPage,
|
||||
component: ExperienceDetailPage,
|
||||
})
|
||||
|
||||
// ── Error page ────────────────────────────────────────────────────────────
|
||||
|
||||
function ExperienceErrorPage() {
|
||||
return (
|
||||
<div className="page-wrap flex min-h-[60vh] flex-col items-center justify-center gap-4 text-center">
|
||||
<p className="island-kicker">Not Found</p>
|
||||
<h1 className="display-title text-4xl">Experience not found</h1>
|
||||
<Link to="/" className="nav-link">
|
||||
← Back to portfolio
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Shared primitives ─────────────────────────────────────────────────────
|
||||
|
||||
const EMPLOYMENT_TYPE_LABEL: Record<'full-time' | 'part-time', string> = {
|
||||
'full-time': 'Full-time',
|
||||
'part-time': 'Part-time',
|
||||
}
|
||||
|
||||
function TechChip({ label }: { readonly label: string }) {
|
||||
return (
|
||||
<span className="inline-block rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-2.5 py-0.5 text-xs font-semibold text-[var(--lagoon-deep)]">
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 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() {
|
||||
const exp: TExperience = Route.useLoaderData()
|
||||
|
||||
const startIso = exp.startDate.slice(0, 7)
|
||||
const endIso = exp.endDate?.slice(0, 7)
|
||||
const duration = computeDuration(startIso, endIso)
|
||||
const startLabel = formatDate(startIso)
|
||||
const endLabel = endIso ? formatDate(endIso) : 'Present'
|
||||
|
||||
return (
|
||||
<main className="page-wrap px-4 pb-16 pt-10">
|
||||
{/* ── Back link ─────────────────────────────────────── */}
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="nav-link inline-flex items-center gap-1.5 text-sm"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* ── Header ────────────────────────────────────────── */}
|
||||
<section className="island-shell rise-in relative overflow-hidden rounded-[2rem] px-6 py-10 sm:px-10 sm:py-12">
|
||||
<div className="pointer-events-none absolute -left-20 -top-20 h-64 w-64 rounded-full bg-[radial-gradient(circle,rgba(79,184,178,0.25),transparent_65%)]" />
|
||||
<div className="pointer-events-none absolute -bottom-20 -right-20 h-64 w-64 rounded-full bg-[radial-gradient(circle,rgba(47,106,74,0.15),transparent_65%)]" />
|
||||
|
||||
<p className="island-kicker mb-2">{exp.company}</p>
|
||||
<h1 className="display-title mb-4 text-4xl font-bold leading-tight text-[var(--sea-ink)]">
|
||||
{exp.role}
|
||||
</h1>
|
||||
|
||||
{/* Meta row */}
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
{exp.type != null && (
|
||||
<span className="rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-2.5 py-0.5 text-xs font-semibold text-[var(--sea-ink-soft)]">
|
||||
{EMPLOYMENT_TYPE_LABEL[exp.type]}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-[var(--sea-ink-soft)]">
|
||||
{startLabel} — {endLabel}
|
||||
</span>
|
||||
<span className="h-1 w-1 rounded-full bg-[var(--lagoon)]" />
|
||||
<span className="text-sm text-[var(--sea-ink-soft)]">{duration}</span>
|
||||
</div>
|
||||
|
||||
{/* Technologies */}
|
||||
{(exp.technologies ?? []).length > 0 && (
|
||||
<div className="mt-5 flex flex-wrap gap-1.5">
|
||||
{(exp.technologies ?? []).map((tech) => (
|
||||
<TechChip key={tech.id} label={tech.name} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Objectives ────────────────────────────────────── */}
|
||||
{(exp.objectives ?? []).length > 0 && (
|
||||
<section className="mt-10">
|
||||
<h2 className="island-kicker mb-4">Responsibilities</h2>
|
||||
<div className="island-shell feature-card rounded-2xl p-6">
|
||||
<ul className="ml-0 list-none space-y-3 p-0">
|
||||
{(exp.objectives ?? []).map((obj) => (
|
||||
<li
|
||||
key={obj.id}
|
||||
className="flex gap-3 text-sm leading-relaxed text-[var(--sea-ink-soft)]"
|
||||
>
|
||||
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-[var(--lagoon)]" />
|
||||
{obj.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Body (markdown) ───────────────────────────────── */}
|
||||
{exp.body != null && exp.body.trim().length > 0 && (
|
||||
<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]}
|
||||
components={{ code: CodeBlock }}
|
||||
>
|
||||
{exp.body}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,42 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { computeDuration, formatDate, CONTACT_EMAIL } from '#/data/portfolio'
|
||||
import type { TExperience, TEducation, TSkill } from '#/types/strapi'
|
||||
import {
|
||||
experiences,
|
||||
educations,
|
||||
skillGroups,
|
||||
computeDuration,
|
||||
formatDate,
|
||||
CONTACT_EMAIL,
|
||||
} from '#/data/portfolio'
|
||||
import type { Experience, Education, SkillGroup } from '#/data/portfolio'
|
||||
fetchExperiences,
|
||||
fetchEducations,
|
||||
fetchProfile,
|
||||
} from '#/data/loaders/portfolio'
|
||||
|
||||
export const Route = createFileRoute('/')({ component: HomePage })
|
||||
export const Route = createFileRoute('/')({
|
||||
loader: async () => {
|
||||
const [experiences, educations, profile] = await Promise.all([
|
||||
fetchExperiences(),
|
||||
fetchEducations(),
|
||||
fetchProfile(),
|
||||
])
|
||||
return { experiences, educations, profile }
|
||||
},
|
||||
errorComponent: StrapiErrorPage,
|
||||
component: HomePage,
|
||||
})
|
||||
|
||||
// ── Error page ────────────────────────────────────────────────────────────
|
||||
|
||||
function StrapiErrorPage() {
|
||||
return (
|
||||
<div className="page-wrap flex min-h-[60vh] flex-col items-center justify-center gap-4 text-center">
|
||||
<p className="island-kicker">Service Unavailable</p>
|
||||
<h1 className="display-title text-4xl">We'll be right back</h1>
|
||||
<p
|
||||
style={{ color: 'var(--sea-ink-soft)' }}
|
||||
className="max-w-md text-base"
|
||||
>
|
||||
The portfolio data is temporarily unavailable. Please check back in a
|
||||
few minutes.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Shared UI primitives ──────────────────────────────────────────────────
|
||||
|
||||
@@ -69,16 +96,23 @@ function SectionHeading({
|
||||
|
||||
// ── Experience card ───────────────────────────────────────────────────────
|
||||
|
||||
const EMPLOYMENT_TYPE_LABEL: Record<'full-time' | 'part-time', string> = {
|
||||
'full-time': 'Full-time',
|
||||
'part-time': 'Part-time',
|
||||
}
|
||||
|
||||
function ExperienceCard({
|
||||
exp,
|
||||
index,
|
||||
}: {
|
||||
readonly exp: Experience
|
||||
readonly exp: TExperience
|
||||
readonly index: number
|
||||
}) {
|
||||
const duration = computeDuration(exp.startDate, exp.endDate ?? undefined)
|
||||
const startLabel = formatDate(exp.startDate)
|
||||
const endLabel = exp.endDate ? formatDate(exp.endDate) : 'Present'
|
||||
const startIso = exp.startDate.slice(0, 7)
|
||||
const endIso = exp.endDate?.slice(0, 7)
|
||||
const duration = computeDuration(startIso, endIso)
|
||||
const startLabel = formatDate(startIso)
|
||||
const endLabel = endIso ? formatDate(endIso) : 'Present'
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -94,15 +128,21 @@ function ExperienceCard({
|
||||
{/* Card */}
|
||||
<div className="island-shell feature-card mb-6 flex-1 rounded-2xl p-5 pb-6">
|
||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<Link
|
||||
to="/experience/$slug"
|
||||
params={{ slug: exp.slug }}
|
||||
className="group min-w-0"
|
||||
>
|
||||
<p className="island-kicker mb-0.5">{exp.company}</p>
|
||||
<h3 className="m-0 text-base font-bold text-[var(--sea-ink)]">
|
||||
<h3 className="m-0 text-base font-bold text-[var(--sea-ink)] transition group-hover:text-[var(--lagoon-deep)]">
|
||||
{exp.role}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="mt-0.5 flex-shrink-0 rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-2.5 py-0.5 text-xs font-semibold text-[var(--sea-ink-soft)]">
|
||||
{exp.type}
|
||||
</span>
|
||||
</Link>
|
||||
{exp.type != null && (
|
||||
<span className="mt-0.5 flex-shrink-0 rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-2.5 py-0.5 text-xs font-semibold text-[var(--sea-ink-soft)]">
|
||||
{EMPLOYMENT_TYPE_LABEL[exp.type]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[var(--sea-ink-soft)]">
|
||||
@@ -111,28 +151,26 @@ function ExperienceCard({
|
||||
</span>
|
||||
<span className="h-1 w-1 rounded-full bg-[var(--lagoon)]" />
|
||||
<span>{duration}</span>
|
||||
<span className="h-1 w-1 rounded-full bg-[var(--lagoon)]" />
|
||||
<span>{exp.location}</span>
|
||||
</div>
|
||||
|
||||
{exp.bullets.length > 0 && (
|
||||
{(exp.objectives ?? []).length > 0 && (
|
||||
<ul className="mb-3 ml-0 list-none space-y-1.5 p-0">
|
||||
{exp.bullets.map((bullet) => (
|
||||
{(exp.objectives ?? []).map((obj) => (
|
||||
<li
|
||||
key={bullet}
|
||||
key={obj.id}
|
||||
className="flex gap-2 text-sm text-[var(--sea-ink-soft)]"
|
||||
>
|
||||
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-[var(--lagoon)]" />
|
||||
{bullet}
|
||||
{obj.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{exp.tech.length > 0 && (
|
||||
{(exp.technologies ?? []).length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{exp.tech.map((t) => (
|
||||
<TechChip key={t} label={t} />
|
||||
{(exp.technologies ?? []).map((tech) => (
|
||||
<TechChip key={tech.id} label={tech.name} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -143,18 +181,22 @@ function ExperienceCard({
|
||||
|
||||
// ── Education card ────────────────────────────────────────────────────────
|
||||
|
||||
function EducationCard({ edu }: { readonly edu: Education }) {
|
||||
function EducationCard({ edu }: { readonly edu: TEducation }) {
|
||||
const startYear = new Date(edu.startDate).getFullYear()
|
||||
const endYear = edu.endDate ? new Date(edu.endDate).getFullYear() : null
|
||||
|
||||
return (
|
||||
<div className="island-shell feature-card rise-in rounded-2xl p-6">
|
||||
<p className="island-kicker mb-2">{edu.institution}</p>
|
||||
<h3 className="mb-2 text-base font-bold leading-snug text-[var(--sea-ink)]">
|
||||
{edu.degree}
|
||||
{edu.fieldOfStudy ? ` ${edu.fieldOfStudy}` : ''}
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[var(--sea-ink-soft)]">
|
||||
<span>
|
||||
{edu.startYear} – {edu.endYear}
|
||||
{startYear} – {endYear ?? 'Present'}
|
||||
</span>
|
||||
{edu.grade !== null && (
|
||||
{edu.grade != null && (
|
||||
<>
|
||||
<span className="h-1 w-1 rounded-full bg-[var(--lagoon)]" />
|
||||
<span className="font-semibold text-[var(--lagoon-deep)]">
|
||||
@@ -169,13 +211,19 @@ function EducationCard({ edu }: { readonly edu: Education }) {
|
||||
|
||||
// ── Skill group card ──────────────────────────────────────────────────────
|
||||
|
||||
function SkillGroupCard({ group }: { readonly group: SkillGroup }) {
|
||||
function SkillGroupCard({
|
||||
category,
|
||||
skills,
|
||||
}: {
|
||||
readonly category: string
|
||||
readonly skills: readonly TSkill[]
|
||||
}) {
|
||||
return (
|
||||
<div className="island-shell feature-card rise-in rounded-2xl p-5">
|
||||
<p className="island-kicker mb-3">{group.category}</p>
|
||||
<p className="island-kicker mb-3">{category}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.skills.map((skill) => (
|
||||
<TechChip key={skill} label={skill} />
|
||||
{skills.map((skill) => (
|
||||
<TechChip key={skill.id} label={skill.name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,6 +233,17 @@ function SkillGroupCard({ group }: { readonly group: SkillGroup }) {
|
||||
// ── Page ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function HomePage() {
|
||||
const { experiences, educations, profile } = Route.useLoaderData()
|
||||
|
||||
const skillsByGroup = profile.skills.reduce<Record<string, TSkill[]>>(
|
||||
(acc, skill) => {
|
||||
const key = skill.group
|
||||
acc[key] = [...(acc[key] ?? []), skill]
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
return (
|
||||
<main className="page-wrap px-4 pb-16 pt-10">
|
||||
{/* ── Hero ──────────────────────────────────────────── */}
|
||||
@@ -197,12 +256,11 @@ function HomePage() {
|
||||
Software Engineer · Copenhagen, Denmark
|
||||
</p>
|
||||
<h1 className="display-title mb-4 max-w-2xl text-5xl font-bold leading-[1.02] tracking-tight text-[var(--sea-ink)] sm:text-7xl">
|
||||
Hi, I'm Mohammad.
|
||||
Hi, I'm {profile.firstname}.
|
||||
</h1>
|
||||
<p className="mb-8 max-w-xl text-base leading-relaxed text-[var(--sea-ink-soft)] sm:text-lg">
|
||||
Crafting robust full-stack solutions with a passion for clean
|
||||
architecture, great developer experience, and shipping products people
|
||||
love.
|
||||
{profile.about ??
|
||||
'Crafting robust full-stack solutions with a passion for clean architecture, great developer experience, and shipping products people love.'}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
@@ -247,8 +305,12 @@ function HomePage() {
|
||||
id="skills"
|
||||
/>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{skillGroups.map((group) => (
|
||||
<SkillGroupCard key={group.category} group={group} />
|
||||
{Object.entries(skillsByGroup).map(([groupKey, skills]) => (
|
||||
<SkillGroupCard
|
||||
key={groupKey}
|
||||
category={groupKey}
|
||||
skills={skills}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* These types match the Strapi Cloud Template Blog schema
|
||||
*/
|
||||
|
||||
import type { Block } from '@/components/blocks'
|
||||
import type { Block } from '#/components/blocks'
|
||||
|
||||
// Base image type from Strapi media library
|
||||
export type TImage = {
|
||||
@@ -88,3 +88,61 @@ export type TStrapiResponse<T = null> = {
|
||||
pagination?: TStrapiPagination
|
||||
}
|
||||
}
|
||||
|
||||
export type TSkill = {
|
||||
readonly id: number
|
||||
readonly name: string
|
||||
readonly group: string
|
||||
readonly link: string | null
|
||||
}
|
||||
|
||||
export type TProfile = {
|
||||
readonly id: number
|
||||
readonly documentId: string
|
||||
readonly firstname: string
|
||||
readonly lastname: string
|
||||
readonly fullname: string
|
||||
readonly phone?: string
|
||||
readonly workStatus: string
|
||||
readonly dateOfBirth?: string
|
||||
readonly about?: string
|
||||
readonly skills: readonly TSkill[]
|
||||
}
|
||||
|
||||
export type TObjective = {
|
||||
readonly id: number
|
||||
readonly description: string
|
||||
}
|
||||
|
||||
export type TTechnology = {
|
||||
readonly id: number
|
||||
readonly name: string
|
||||
readonly group: 'frontend' | 'backend' | 'database' | 'infrastructure'
|
||||
readonly link: string | null
|
||||
}
|
||||
|
||||
export type TExperience = {
|
||||
readonly id: number
|
||||
readonly documentId: string
|
||||
readonly company: string
|
||||
readonly role: string
|
||||
readonly slug: string
|
||||
readonly body: string | null
|
||||
readonly startDate: string
|
||||
readonly endDate: string | null
|
||||
readonly type: 'full-time' | 'part-time' | null
|
||||
readonly locale?: string
|
||||
readonly objectives?: readonly TObjective[]
|
||||
readonly technologies?: readonly TTechnology[]
|
||||
}
|
||||
|
||||
export type TEducation = {
|
||||
readonly id: number
|
||||
readonly documentId: string
|
||||
readonly institution: string
|
||||
readonly degree: string
|
||||
readonly fieldOfStudy?: string
|
||||
readonly startDate: string
|
||||
readonly endDate: string | null
|
||||
readonly grade: string | null
|
||||
}
|
||||
|
||||
1521
ts/package-lock.json
generated
1521
ts/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
];
|
||||
@@ -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 },
|
||||
);
|
||||
@@ -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) },
|
||||
);
|
||||
@@ -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
10
ts/src/types.d.ts
vendored
@@ -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;
|
||||
}
|
||||
113
ts/tsconfig.json
113
ts/tsconfig.json
@@ -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. */
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user