Implement Contact Page #1

Merged
baobeld merged 3 commits from contact into main 2025-04-30 16:54:45 -04:00
11 changed files with 245 additions and 117 deletions

View file

@ -1,48 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
pull_request:
branches: ['main']
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- uses: oven-sh/setup-bun@v2
- name: Install
run: bun install
- name: Build
run: bun run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: './dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -5,6 +5,11 @@ on:
push: push:
branches: branches:
- main - main
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs: jobs:
install: install:
name: Install name: Install

35
.github/workflows/pr.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: PR Gate
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
checks:
name: Checks
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Bun Setup
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.tool-versions'
- name: Install
run: bun install
- name: Lint
run: bun lint
- name: Build
run: bun run build
- name: Svelte Check
run: bun check
- name: Test
run: bun run test:unit

View file

@ -4,32 +4,34 @@
"": { "": {
"name": "benjaminpalko.github.io", "name": "benjaminpalko.github.io",
"dependencies": { "dependencies": {
"lucide-svelte": "latest", "lucide-svelte": "^0.503.0",
"nodemailer": "^6.10.1",
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "latest", "@eslint/compat": "^1.2.8",
"@eslint/js": "latest", "@eslint/js": "^9.25.1",
"@sveltejs/adapter-node": "latest", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "latest", "@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "latest", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "latest", "@tailwindcss/vite": "^4.1.4",
"autoprefixer": "latest", "@types/nodemailer": "^6.4.17",
"daisyui": "latest", "autoprefixer": "^10.4.21",
"eslint": "latest", "daisyui": "^5.0.28",
"eslint-config-prettier": "latest", "eslint": "^9.25.1",
"eslint-plugin-svelte": "latest", "eslint-config-prettier": "^10.1.2",
"globals": "latest", "eslint-plugin-svelte": "^3.5.1",
"mdsvex": "latest", "globals": "^16.0.0",
"prettier": "latest", "mdsvex": "^0.12.5",
"prettier-plugin-svelte": "latest", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "latest", "prettier-plugin-svelte": "^3.3.3",
"svelte": "latest", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte-check": "latest", "svelte": "^5.28.2",
"tailwindcss": "latest", "svelte-check": "^4.1.6",
"typescript": "latest", "tailwindcss": "^4.1.4",
"typescript-eslint": "latest", "typescript": "^5.8.3",
"vite": "latest", "typescript-eslint": "^8.31.0",
"vitest": "latest", "vite": "^6.3.3",
"vitest": "^3.1.2",
}, },
}, },
}, },
@ -228,6 +230,10 @@
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
"@types/nodemailer": ["@types/nodemailer@6.4.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
@ -508,6 +514,8 @@
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"nodemailer": ["nodemailer@6.10.1", "", {}, "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="],
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
"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=="], "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=="],
@ -632,6 +640,8 @@
"typescript-eslint": ["typescript-eslint@8.31.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", "@typescript-eslint/utils": "8.31.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ=="], "typescript-eslint": ["typescript-eslint@8.31.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", "@typescript-eslint/utils": "8.31.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unist-util-is": ["unist-util-is@4.1.0", "", {}, "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg=="], "unist-util-is": ["unist-util-is@4.1.0", "", {}, "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg=="],
"unist-util-stringify-position": ["unist-util-stringify-position@2.0.3", "", { "dependencies": { "@types/unist": "^2.0.2" } }, "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g=="], "unist-util-stringify-position": ["unist-util-stringify-position@2.0.3", "", { "dependencies": { "@types/unist": "^2.0.2" } }, "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g=="],

1
devops/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
data/

View file

@ -0,0 +1,10 @@
services:
postgres:
container_name: postgres
image: postgres
environment:
POSTGRES_DB: ${DATABASE_NAME}
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
volumes:
- ./data:/var/lib/postgresql/data

View file

@ -5,6 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"compose:up": "docker compose -p strapi -f devops/docker-compose.dev.yml up -d ",
"compose:down": "docker compose -p strapi -f devops/docker-compose.dev.yml down",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@ -21,6 +23,7 @@
"@sveltejs/kit": "^2.20.7", "@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@types/nodemailer": "^6.4.17",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"daisyui": "^5.0.28", "daisyui": "^5.0.28",
"eslint": "^9.25.1", "eslint": "^9.25.1",
@ -40,6 +43,7 @@
"vitest": "^3.1.2" "vitest": "^3.1.2"
}, },
"dependencies": { "dependencies": {
"lucide-svelte": "^0.503.0" "lucide-svelte": "^0.503.0",
"nodemailer": "^6.10.1"
} }
} }

View file

@ -40,7 +40,7 @@
</div> </div>
{/snippet} {/snippet}
<div class="bg-hazard rounded-box w-full border border-amber-400 p-6"> <div class="bg-hazard rounded-box h-full w-full border border-amber-400 p-6">
<div <div
class="bg-base-200 rounded-box flex h-full flex-col items-center justify-center gap-48 overflow-hidden border border-amber-400" class="bg-base-200 rounded-box flex h-full flex-col items-center justify-center gap-48 overflow-hidden border border-amber-400"
> >

View file

@ -24,20 +24,22 @@
</script> </script>
<div class="h-screen w-screen p-12"> <div class="h-screen w-screen p-12">
<div class="bg-base-200 flex min-h-full flex-col rounded-4xl"> <div
<div class="p-8"> class="bg-base-200 grid min-h-full grid-rows-[max-content_max-content_auto_max-content] rounded-4xl"
>
<header class=" p-8">
<Navbar title="Benjamin Palko" {pages} route={page.route} /> <Navbar title="Benjamin Palko" {pages} route={page.route} />
</div> </header>
{#if current && current.slug !== '/'} {#if current && current.slug !== '/'}
<div class="bg-base-300 w-full px-12 py-8"> <div class="bg-base-300 w-full px-12 py-8">
<h1 class="text-4xl font-light">{current.name}</h1> <h1 class="text-4xl font-light">{current.name}</h1>
</div> </div>
{/if} {/if}
<div class="flex flex-1 items-stretch justify-center p-8"> <main>
{@render children()} {@render children()}
</div> </main>
<div class="p-8"> <footer class="p-8">
<Footer links={socialLinks} /> <Footer links={socialLinks} />
</div> </footer>
</div> </div>
</div> </div>

View file

@ -0,0 +1,87 @@
import {
MAIL_TRANSPORT_HOST,
MAIL_TRANSPORT_PASS,
MAIL_TRANSPORT_PORT,
MAIL_TRANSPORT_USER,
} from "$env/static/private";
import { fail } from "@sveltejs/kit";
import { createTransport } from "nodemailer";
import type { Actions } from "./$types";
export const actions = {
default: async ({ request }) => {
const form = await request.formData();
if (!form.has("name")) {
return fail(400, { name: null, missing: true });
}
if (!form.has("email")) {
return fail(400, { email: null, missing: true });
}
if (!form.has("subject")) {
return fail(400, { subject: null, missing: true });
}
if (!form.has("message")) {
return fail(400, { message: null, missing: true });
}
const name = form.get("name");
if (typeof name !== "string") {
return fail(400, { name, incorrect: true });
}
const email = form.get("email");
if (typeof email !== "string") {
return fail(400, { email, incorrect: true });
}
const subject = form.get("subject");
if (typeof subject !== "string") {
return fail(400, { subject, incorrect: true });
}
const message = form.get("message");
if (typeof message !== "string") {
return fail(400, { message, incorrect: true });
}
const transport = createTransport({
host: MAIL_TRANSPORT_HOST,
port: isNaN(Number(MAIL_TRANSPORT_PORT))
? 587
: Number(MAIL_TRANSPORT_PORT),
auth: {
user: MAIL_TRANSPORT_USER,
pass: MAIL_TRANSPORT_PASS,
},
});
try {
const info1 = await transport.sendMail({
from: "no-reply@benjaminpalko.ca",
to: email,
subject: `Thank you for reaching out`,
text: `Hello ${name}!\n\nThank you for reaching out! I will be sure to get back to you ASAP.\n\nCheers,\nBenjamin`,
});
const info2 = await transport.sendMail({
from: email,
to: "contact@benjaminpalko.ca",
subject: subject,
text: message,
});
return {
response: {
accepted: [...info1.accepted, ...info2.accepted],
rejected: [...info1.rejected, ...info2.rejected],
},
};
} catch (error) {
console.error(error);
}
},
} satisfies Actions;

View file

@ -1,41 +1,63 @@
<script> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { Croissant, Mail, UserRound } from 'lucide-svelte'; import {
CircleCheck,
CircleX,
Croissant,
Mail,
Send,
TriangleAlert,
UserRound
} from 'lucide-svelte';
import type { PageProps } from './$types';
let { form }: PageProps = $props();
</script> </script>
<div class="flex flex-col"> <form id="contact" method="post" use:enhance>
<div class="flex flex-col items-center"> <div class="mx-auto grid max-w-3xl grid-flow-col grid-cols-3 grid-rows-7 content-end gap-4 py-8">
<span class="animate-bounce text-2xl">😁</span> <div class="col-span-full">
<h2 class="text-2xl">Hello there friend</h2> {#if form?.response?.rejected.length ?? 0 > 0}
<div role="alert" class="alert alert-error w-full">
<CircleX />
<span>Ruh roh raggy!</span>
</div>
{:else if form?.response?.accepted.length ?? 0 > 0}
<div role="alert" class="alert alert-success w-full">
<CircleCheck />
<span>Your message has been sent successfully!</span>
</div>
{:else if form?.incorrect}
<div role="alert" class="alert alert-warning w-full">
<TriangleAlert />
<span>Ruh roh raggy!</span>
</div>
{/if}
</div> </div>
<form id="contact" use:enhance> <h2 class="col-span-full text-3xl font-light">Reach out! 🤝</h2>
<div class="bg-base-200 flex flex-col gap-4">
<h1 class="text-4xl">Want to chat?</h1> <label class="input input-bordered flex w-full items-center">
<h5 class="text-lg">Reach out! 🤝</h5>
<label class="input input-bordered flex items-center gap-2">
<UserRound /> <UserRound />
<input name="name" type="text" class="grow" placeholder="What is your name?" /> <input name="name" type="text" class="grow" placeholder="Full Name" />
</label> </label>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Mail /> <Mail />
<input name="email" type="text" class="grow" placeholder="I'm going to need your email." /> <input name="email" type="text" class="grow" placeholder="Your Email" />
</label> </label>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Croissant /> <Croissant />
<input name="name" type="text" class="grow" placeholder="Subject" /> <input name="subject" type="text" class="grow" placeholder="Subject" />
</label> </label>
<label class="form-control"> <button type="submit" class="btn btn-primary">Send Message <Send size={20} /></button>
<div class="label">
<span class="label-text">What do you want to say?</span> <div class="col-span-2 row-span-4">
</div>
<textarea <textarea
name="message" name="message"
form="contact" form="contact"
class="textarea textarea-bordered h-24" class="textarea textarea-bordered h-full w-full resize-none"
placeholder="Hello friend..." placeholder="Message"
></textarea> ></textarea>
</label> </div>
</div> </div>
</form> </form>
</div>