diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..a45888c Binary files /dev/null and b/.DS_Store differ diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..476bb29 --- /dev/null +++ b/.air.toml @@ -0,0 +1,37 @@ +root = "." +tmp_dir = "tmp" + +[build] + bin = "./tmp/main" + cmd = "templ generate && go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor"] + exclude_file = [] + exclude_regex = [".*_templ.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "templ", "html"] + kill_delay = "0s" + log = "build-errors.log" + send_interrupt = false + stop_on_error = true + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + time = false + +[misc] + clean_on_exit = false + +[proxy] + enabled = true + proxy_port = 8383 + app_port = 8282 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c19a3ab --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + packages: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache: true + - uses: ko-build/setup-ko@v0.7 + - uses: sigstore/cosign-installer@v3.7.0 + 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 }}' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..24802c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Build. +FROM golang:1.24.0 AS build-stage +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 /entrypoint /entrypoint +COPY --from=build-stage /app/assets /assets +EXPOSE 8080 +USER nonroot:nonroot +ENTRYPOINT ["/entrypoint"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2bae59e --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +generate: + templ generate --watch + +server: + air diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..fb3bea6 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/images/invoice-manager-thumbnail.png b/assets/images/invoice-manager-thumbnail.png new file mode 100644 index 0000000..219149a Binary files /dev/null and b/assets/images/invoice-manager-thumbnail.png differ diff --git a/assets/js/dateUtils.js b/assets/js/dateUtils.js new file mode 100644 index 0000000..64eedbb --- /dev/null +++ b/assets/js/dateUtils.js @@ -0,0 +1,40 @@ +function calculateDuration(startDate, endDate) { + const start = new Date(startDate); + const end = endDate ? new Date(endDate) : new Date(); + + const years = end.getFullYear() - start.getFullYear(); + const months = end.getMonth() - start.getMonth(); + + let totalMonths = years * 12 + months; + if (end.getDate() < start.getDate()) { + totalMonths--; + } + + const finalYears = Math.floor(totalMonths / 12); + const finalMonths = totalMonths % 12; + + let duration = ""; + if (finalYears > 0) { + duration += `${finalYears} year${finalYears > 1 ? "s" : ""}`; + } + if (finalMonths > 0) { + if (duration) duration += " "; + duration += `${finalMonths} month${finalMonths > 1 ? "s" : ""}`; + } + return duration; +} + +function formatDateRange(startDate, endDate) { + const start = new Date(startDate); + const end = endDate ? new Date(endDate) : new Date(); + + const formatDate = (date) => { + return date.toLocaleDateString("en-US", { + month: "short", + year: "numeric", + }); + }; + + const duration = calculateDuration(startDate, endDate); + return `${formatDate(start)} - ${endDate ? formatDate(end) : "Present"} ยท ${duration}`; +} diff --git a/assets/js/landing-page-animations.js b/assets/js/landing-page-animations.js new file mode 100644 index 0000000..23c9bb1 --- /dev/null +++ b/assets/js/landing-page-animations.js @@ -0,0 +1,23 @@ +const { animate, scroll } = Motion; + +const yearsExperience = document.getElementById("years-experience"); +const programmingLangues = document.getElementById("programming-languages"); + +const startDate = new Date("2019-01-01"); +years = Math.floor((new Date() - startDate) / 1000 / 60 / 60 / 24 / 365); + +animate(0, years, { + duration: 2, + ease: "circOut", + onUpdate: (latest) => { + yearsExperience.innerHTML = Math.round(latest); + }, +}); + +animate(0, 4, { + duration: 2, + ease: "circOut", + onUpdate: (latest) => { + programmingLangues.innerHTML = Math.round(latest); + }, +}); diff --git a/assets/json/experiences.json b/assets/json/experiences.json new file mode 100644 index 0000000..890c979 --- /dev/null +++ b/assets/json/experiences.json @@ -0,0 +1,126 @@ +[ + { + "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 websites and internal tools. I am also involved in the development of new features and improvements to the existing systems.", + "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": "" }] + } + } +] diff --git a/assets/json/projects.json b/assets/json/projects.json new file mode 100644 index 0000000..03571f8 --- /dev/null +++ b/assets/json/projects.json @@ -0,0 +1,22 @@ +[ + { + "name": "Invoice Management System", + "start": null, + "end": null, + "stack": ["Go", "Wails", "React", "SQLite3", "TailwindCSS"], + "description": "Invoie Management System that helps mechanics create invoices, manage customers, finances and more.", + "thumbnail": "/static/images/invoice-manager-thumbnail.png", + "github_link": "", + "status": "In Progress" + }, + { + "name": "Portfolio", + "start": null, + "end": null, + "stack": ["Go", "Templ", "Motion", "TailwindCSS"], + "description": "The website you are currently on. A portfolio website that showcases my projects, experiences and skills.", + "thumbnail": "", + "github_link": "", + "status": "Completed" + } +] diff --git a/components/about_page.templ b/components/about_page.templ new file mode 100644 index 0000000..8e6dabf --- /dev/null +++ b/components/about_page.templ @@ -0,0 +1,12 @@ +package components + +templ AboutPage() { + @rootLayout() { +
+
+

About Me

+

I am a software engineer with a passion for web development. I have experience with a variety of technologies including React, Node.js, and MongoDB. I am always looking to learn new things and improve my skills.

+
+
+ } +} diff --git a/components/about_page_templ.go b/components/about_page_templ.go new file mode 100644 index 0000000..0224b73 --- /dev/null +++ b/components/about_page_templ.go @@ -0,0 +1,58 @@ +// 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 AboutPage() 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, "

About Me

I am a software engineer with a passion for web development. I have experience with a variety of technologies including React, Node.js, and MongoDB. I am always looking to learn new things and improve my skills.

") + 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 diff --git a/components/contact_page.templ b/components/contact_page.templ new file mode 100644 index 0000000..186490e --- /dev/null +++ b/components/contact_page.templ @@ -0,0 +1,109 @@ +package components + +templ ContactPage() { + @rootLayout() { +
+
+
+

Get in Touch

+

Have a question or want to work together? Let's connect!

+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + } +} diff --git a/components/contact_page_templ.go b/components/contact_page_templ.go new file mode 100644 index 0000000..8b1051d --- /dev/null +++ b/components/contact_page_templ.go @@ -0,0 +1,58 @@ +// 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 ContactPage() 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, "

Get in Touch

Have a question or want to work together? Let's connect!

") + 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 diff --git a/components/experience_card.templ b/components/experience_card.templ new file mode 100644 index 0000000..0869e97 --- /dev/null +++ b/components/experience_card.templ @@ -0,0 +1,46 @@ +package components + +import "github.com/moh682/portfolio/domain" +import "slices" +import "strings" + +templ experienceCard(exp domain.Experience) { +
+
+ +
+

{ exp.Job.Title }

+
+ { exp.Company.Name } +

{ exp.Duration() }

+

{ exp.GetStartDate() } - { exp.GetEndDate() }

+ { exp.Company.City } +
+
+ for _, stack := range slices.Concat(exp.Job.FrontendStack, exp.Job.BackendStack, exp.Job.InfrastructureStack) { + @techStack(stack) + } +
+
+ +
+ +
+
+

About { exp.Company.Name }

+ for _, description := range strings.Split(exp.Company.Description, "\n") { +

+ { description } +

+ } +
+
+

Role & Responsibilities

+
+ { exp.Job.Description } +
+
+
+
+
+} diff --git a/components/experience_card_templ.go b/components/experience_card_templ.go new file mode 100644 index 0000000..c60a033 --- /dev/null +++ b/components/experience_card_templ.go @@ -0,0 +1,199 @@ +// 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, "

") + 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/experience_card.templ`, Line: 12, Col: 65} + } + _, 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, "

") + 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/experience_card.templ`, Line: 14, Col: 113} + } + _, 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, "

") + 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/experience_card.templ`, Line: 15, Col: 48} + } + _, 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, "

") + 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/experience_card.templ`, Line: 16, Col: 59} + } + _, 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/experience_card.templ`, Line: 16, Col: 82} + } + _, 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, "

") + 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/experience_card.templ`, Line: 17, Col: 100} + } + _, 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, "
") + 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, "

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/experience_card.templ`, Line: 30, Col: 61} + } + _, 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, "

") + 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, "

") + 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/experience_card.templ`, Line: 33, Col: 20} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Role & Responsibilities

") + 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/experience_card.templ`, Line: 40, Col: 27} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/components/experiences_page.templ b/components/experiences_page.templ new file mode 100644 index 0000000..861a0c2 --- /dev/null +++ b/components/experiences_page.templ @@ -0,0 +1,63 @@ + +package components + +import "github.com/moh682/portfolio/domain" +import "fmt" + +templ ExperiencesPage(experiences []domain.Experience) { + @rootLayout() { +
+
+ +
+
+ for i := range experiences { +
+
+
+ } +
+
+
+
+
+

Professional Experience

+

Building and scaling software solutions that matter

+
+
+
+
+
+
+ for i, exp := range experiences { +
+
+
+
+
+
+
+
+
+ @experienceCard(exp) +
+ } +
+
+
+
+
+
+ } +} diff --git a/components/experiences_page_templ.go b/components/experiences_page_templ.go new file mode 100644 index 0000000..b279ca9 --- /dev/null +++ b/components/experiences_page_templ.go @@ -0,0 +1,239 @@ +// 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 "fmt" + +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_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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i := range experiences { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 = []any{"h-12 w-1 mb-4 transition-all duration-300 cursor-pointer hover:bg-emerald-400"} + 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, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Professional Experience

Building and scaling software solutions that matter

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, exp := range experiences { + var templ_7745c5c3_Var8 = []any{"absolute w-full transition-all opacity-0 duration-700 ease-out translate-y-10"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 = []any{fmt.Sprintf("relative flex w-full h-full items-center justify-center translate-y-[%dpx]", i*4*32)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 = []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_Var14...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + 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 diff --git a/components/external_links.templ b/components/external_links.templ new file mode 100644 index 0000000..55206cb --- /dev/null +++ b/components/external_links.templ @@ -0,0 +1,14 @@ +package components + +var iconSize = "28" + +templ externalLinks() { +
+ + + + + + +
+} diff --git a/components/external_links_templ.go b/components/external_links_templ.go new file mode 100644 index 0000000..f1b3daa --- /dev/null +++ b/components/external_links_templ.go @@ -0,0 +1,94 @@ +// 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/components/landing_page.templ b/components/landing_page.templ new file mode 100644 index 0000000..83f39f6 --- /dev/null +++ b/components/landing_page.templ @@ -0,0 +1,71 @@ +package components + +templ LandingPage() { + @rootLayout() { + +
+ +
+
+
+ +
+
+ Software Developer +

+ Hey there! I'm Mohammad Hariri +

+

+ I turn complex problems into elegant code solutions. Passionate about building robust applications that make a real difference in people's lives. +

+
+ + + +
+ +
+
+
+ Mohammad Hariri +
+
+
+ +
+
+
0
+
Years Experience
+
+
+
0
+
Programming Languages
+
+
+
+
+
+ } +} diff --git a/components/landing_page_templ.go b/components/landing_page_templ.go new file mode 100644 index 0000000..0f4261b --- /dev/null +++ b/components/landing_page_templ.go @@ -0,0 +1,58 @@ +// 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, "
Software Developer

Hey there! I'm Mohammad Hariri

I turn complex problems into elegant code solutions. Passionate about building robust applications that make a real difference in people's lives.

\"Mohammad
0
Years Experience
0
Programming Languages
") + 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 diff --git a/components/layout.templ b/components/layout.templ new file mode 100644 index 0000000..a342535 --- /dev/null +++ b/components/layout.templ @@ -0,0 +1,58 @@ +package components + +templ rootLayout() { + + + + + + + Mohammad's Portfolio + + + + @navbar() +
+ { children... } +
+ + +} diff --git a/components/layout_templ.go b/components/layout_templ.go new file mode 100644 index 0000000..0e8a9d8 --- /dev/null +++ b/components/layout_templ.go @@ -0,0 +1,56 @@ +// 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, "Mohammad's Portfolio") + 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + 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, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/components/logo.templ b/components/logo.templ new file mode 100644 index 0000000..d84e20a --- /dev/null +++ b/components/logo.templ @@ -0,0 +1,7 @@ +package components + +import "fmt" + +templ Logo(size int) { + +} diff --git a/components/logo_templ.go b/components/logo_templ.go new file mode 100644 index 0000000..92260bf --- /dev/null +++ b/components/logo_templ.go @@ -0,0 +1,55 @@ +// 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 "fmt" + +func Logo(size int) 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/components/navbar.templ b/components/navbar.templ new file mode 100644 index 0000000..db8cf0b --- /dev/null +++ b/components/navbar.templ @@ -0,0 +1,28 @@ +package components + +templ navbar() { + +} diff --git a/components/navbar_templ.go b/components/navbar_templ.go new file mode 100644 index 0000000..5c944d9 --- /dev/null +++ b/components/navbar_templ.go @@ -0,0 +1,48 @@ +// 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/components/notfound.templ b/components/notfound.templ new file mode 100644 index 0000000..6178995 --- /dev/null +++ b/components/notfound.templ @@ -0,0 +1,10 @@ +package components + +templ NotFound() { + @rootLayout() { +
+

404

+

Page not found

+
+ } +} diff --git a/components/notfound_templ.go b/components/notfound_templ.go new file mode 100644 index 0000000..15b5dea --- /dev/null +++ b/components/notfound_templ.go @@ -0,0 +1,58 @@ +// 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, "

404

Page not found

") + 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 diff --git a/components/projects_page.templ b/components/projects_page.templ new file mode 100644 index 0000000..04a26e5 --- /dev/null +++ b/components/projects_page.templ @@ -0,0 +1,65 @@ +package components + +import "github.com/moh682/portfolio/domain" + +templ tag(tag string) { + { tag } +} + +templ status(status string) { + { status } +} + +templ projectCard(project domain.Project) { +
+
+ { +
+
+
+
+

{ project.Name }

+ @status(project.Status) +

{ project.Description }

+
+ for _, t := range project.Stack { + @tag(t) + } +
+
+ if project.GithubLink != "" { + + View Project + + + + + } +
+
+} + +templ ProjectsPage(projects []domain.Project) { + @rootLayout() { +
+
+

Projects

+

A collection of projects I've worked on

+
+ for _, project := range projects { + @projectCard(project) + } +
+
+
+ } +} diff --git a/components/projects_page_templ.go b/components/projects_page_templ.go new file mode 100644 index 0000000..897ac94 --- /dev/null +++ b/components/projects_page_templ.go @@ -0,0 +1,290 @@ +// 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, "") + 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, "") + 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, "") + 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, "") + 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, "
\"")

") + 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, "

") + 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, "

") + 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, "

") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if project.GithubLink != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "View Project ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + 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, "

Projects

A collection of projects I've worked on

") + 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, "
") + 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 diff --git a/components/tech_stack.templ b/components/tech_stack.templ new file mode 100644 index 0000000..ed8f340 --- /dev/null +++ b/components/tech_stack.templ @@ -0,0 +1,24 @@ +package components + +import "github.com/moh682/portfolio/domain" + +templ techStack(stack domain.Stack) { + if stack.Link != "" { + + if stack.Logo != "" { + { + } + { stack.Name } + + } else { +
+ if stack.Logo != "" { + { + } + { stack.Name } +
+ } +} diff --git a/components/tech_stack_templ.go b/components/tech_stack_templ.go new file mode 100644 index 0000000..5790a5e --- /dev/null +++ b/components/tech_stack_templ.go @@ -0,0 +1,156 @@ +// 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if stack.Logo != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"") ") + 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_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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if stack.Logo != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"") ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/deploy_script b/deploy_script new file mode 100644 index 0000000..3719744 --- /dev/null +++ b/deploy_script @@ -0,0 +1,2 @@ + + - diff --git a/domain/experience.go b/domain/experience.go new file mode 100644 index 0000000..031c901 --- /dev/null +++ b/domain/experience.go @@ -0,0 +1,83 @@ +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") +} diff --git a/domain/project.go b/domain/project.go new file mode 100644 index 0000000..b0d04d8 --- /dev/null +++ b/domain/project.go @@ -0,0 +1,14 @@ +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"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ad75994 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/moh682/portfolio + +go 1.24.0 + +require github.com/a-h/templ v0.3.833 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bbe292f --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..001a8d6 --- /dev/null +++ b/main.go @@ -0,0 +1,114 @@ +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 { + t := templ.Handler(component, templ.WithStatus(200), templ.WithContentType("text/html")) + + log.Println("templHandlerWithOptions: ", t.Status) + + return t +} + +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("path", r.URL.Path) + + // Only allow specific file extensions + if allowed := func(path string) bool { + allowedExts := map[string]bool{ + ".js": true, + ".css": true, + ".png": true, + ".jpg": true, + ".jpeg": true, + ".svg": true, + ".ico": true, + } + for ext := range allowedExts { + if strings.HasSuffix(path, ext) { + 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("static") + 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 /", templHandlerWithOptions(components.NotFound())) + + // 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), + ), + ), + ) +} diff --git a/middlewares/combine.go b/middlewares/combine.go new file mode 100644 index 0000000..f87dd58 --- /dev/null +++ b/middlewares/combine.go @@ -0,0 +1,11 @@ +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) + } + }) +} diff --git a/middlewares/logger.go b/middlewares/logger.go new file mode 100644 index 0000000..2091213 --- /dev/null +++ b/middlewares/logger.go @@ -0,0 +1,33 @@ +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) { + rw := newStatusResponseWriter(w) + now := time.Now() + next.ServeHTTP(rw, r) + since := time.Since(now) + log.Println(r.Method, r.URL.Path, r.RemoteAddr, rw.status, since) + }) +} + +func newStatusResponseWriter(w http.ResponseWriter) *statusResponseWriter { + return &statusResponseWriter{ResponseWriter: w} +} diff --git a/middlewares/notfound.go b/middlewares/notfound.go new file mode 100644 index 0000000..913afc1 --- /dev/null +++ b/middlewares/notfound.go @@ -0,0 +1,20 @@ +package middlewares + +import ( + "log" + "net/http" + + "github.com/a-h/templ" +) + +func NotFound(next http.Handler, component templ.Component) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + res := newStatusResponseWriter(w) + log.Println("before NotFound: ", res.status) + next.ServeHTTP(res, r) + log.Println("after NotFound: ", res.status) + if res.status == 0 { + } + }) +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..dbe59de --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,22 @@ +export default { + content: ["./components/**/*.{js,ts,jsx,tsx,templ}"], + theme: { + extend: { + animation: { + fadeIn: "fadeIn 1s ease-in forwards", + slideIn: "slideIn 1s ease-out forwards", + }, + keyframes: { + fadeIn: { + "0%": { opacity: "0" }, + "100%": { opacity: "1" }, + }, + slideIn: { + "0%": { opacity: "0", transform: "translateY(20px)" }, + "100%": { opacity: "1", transform: "translateY(0)" }, + }, + }, + }, + }, + plugins: [], +}; diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..f50898d --- /dev/null +++ b/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..2156487 Binary files /dev/null and b/tmp/main differ