work on porting over contact form from old site, also added initial db support

to use later
This commit is contained in:
badblocks 2026-01-06 08:07:19 -08:00
parent 8d989ef36f
commit f641dac69b
No known key found for this signature in database
12 changed files with 232 additions and 32 deletions

3
.gitignore vendored
View file

@ -13,9 +13,8 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
# prod environment variables
.env
.env.production
# macOS-specific files
.DS_Store

View file

@ -1,25 +1,52 @@
import htmx from "astro-htmx";
// @ts-check
import { defineConfig } from "astro/config";
import { defineConfig, envField } from "astro/config";
import alpinejs from "@astrojs/alpinejs";
import partytown from "@astrojs/partytown";
import sitemap from "@astrojs/sitemap";
import bun from "@nurodev/astro-bun";
import db from "@astrojs/db";
// https://astro.build/config
export default defineConfig({
site: "https://badblocks.dev",
trailingSlash: "never",
adapter: bun(),
output: "static",
integrations: [
alpinejs(),
partytown(),
sitemap(),
htmx(),
// sitemap({
// filter: (page) =>
// page !== "https://example.com/secret-vip-lounge-1/" &&
// page !== "https://example.com/secret-vip-lounge-2/",
// }),
],
devToolbar: { enabled: false },
prefetch: {
prefetchAll: true,
},
security: {
checkOrigin: true,
},
server: {
host: true,
port: 4321,
},
env: {
schema: {
ANDROID_SMS_GATEWAY_LOGIN: envField.string({
context: "server",
access: "secret",
}),
ANDROID_SMS_GATEWAY_PASSWORD: envField.string({
context: "server",
access: "secret",
}),
ANDROID_SMS_GATEWAY_RECIPIENT_PHONE: envField.string({
context: "server",
access: "secret",
}),
ANDROID_SMS_GATEWAY_URL: envField.string({
context: "server",
access: "secret",
}),
},
},
integrations: [alpinejs(), sitemap(), htmx(), db()],
experimental: {
preserveScriptOrder: true,
chromeDevtoolsWorkspace: true,
failOnPrerenderConflict: true,
},
});

BIN
bun.lockb

Binary file not shown.

View file

@ -5,20 +5,30 @@
"scripts": {
"dev": "astro dev",
"start": "bun run ./dist/server/entry.mjs",
"build": "astro build",
"check": "astro check",
"build-only": "astro build",
"build": "astro check && astro build",
"build-remote": "astro check && astro build --remote",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/alpinejs": "^0.4.9",
"@astrojs/check": "^0.9.6",
"@astrojs/db": "^0.18.3",
"@astrojs/partytown": "^2.1.4",
"@astrojs/sitemap": "^3.6.0",
"@astrojs/ts-plugin": "^1.10.6",
"@cap.js/server": "^4.0.5",
"@cap.js/widget": "^0.1.33",
"@nurodev/astro-bun": "^2.1.2",
"@types/alpinejs": "^3.13.11",
"alpinejs": "^3.15.3",
"android-sms-gateway": "^3.0.0",
"astro": "^5.16.6",
"astro-htmx": "^1.0.6",
"htmx.org": "^2.0.8"
"htmx.org": "^2.0.8",
"typescript": "^5.9.3"
},
"devDependencies": {
"@types/bun": "^1.3.5"

View file

@ -13,6 +13,12 @@
href="https://fonts.googleapis.com/css2?family=VT323&display=swap"
rel="stylesheet"
/>
<script>
const yearSpan = document.querySelector("#copyright-year");
if (yearSpan) {
yearSpan.innerHTML = new Date().getFullYear().toString();
}
</script>
<style>
:root {
--min-body-width: 30ch;
@ -96,9 +102,7 @@
</main>
<footer>
<p>
&copy; <script is:inline>
document.write(new Date().getFullYear());
</script>
&copy; <span id="copyright-year">2026</span>
badblocks
</p>
<p>Made from scratch with BAHA: Bun, Astro, Htmx, and Alpine!</p>

View file

@ -0,0 +1,46 @@
const httpFetchClient = {
get: async (url: string, headers: Record<string, string>) => {
const response = await fetch(url, {
method: "GET",
headers,
});
return response.json();
},
post: async (url: string, body: JSON, headers: Record<string, string>) => {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
});
return response.json();
},
put: async (url: string, body: JSON, headers: Record<string, string>) => {
const response = await fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
});
return response.json();
},
patch: async (url: string, body: JSON, headers: Record<string, string>) => {
const response = await fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
});
return response.json();
},
delete: async (url: string, headers: Record<string, string>) => {
const response = await fetch(url, {
method: "DELETE",
headers,
});
return response.json();
},
};
export default httpFetchClient;

View file

@ -0,0 +1,53 @@
import Client from "android-sms-gateway";
import {
ANDROID_SMS_GATEWAY_LOGIN,
ANDROID_SMS_GATEWAY_PASSWORD,
ANDROID_SMS_GATEWAY_RECIPIENT_PHONE,
ANDROID_SMS_GATEWAY_URL,
} from "astro:env/server";
import httpFetchClient from "@lib/HttpFetchClient";
class SmsClient {
readonly api: Client;
constructor() {
this.api = new Client(
ANDROID_SMS_GATEWAY_LOGIN,
ANDROID_SMS_GATEWAY_PASSWORD,
httpFetchClient,
ANDROID_SMS_GATEWAY_URL,
);
}
async sendSMS(message: string) {
const bundle = {
phoneNumbers: [ANDROID_SMS_GATEWAY_RECIPIENT_PHONE], // hard-coded on purpose ;)
message: message,
};
try {
const msg_state = await this.api.send(bundle);
return {
success: true,
id: msg_state.id,
state: msg_state.state,
};
} catch (error) {
return { success: false, error: error };
}
}
async update(id: string) {
try {
const msg_state = await this.api.getState(id);
return {
success: true,
id: msg_state.id,
state: msg_state.state,
};
} catch (error) {
return { success: false, id: id, error: error };
}
}
}
export default SmsClient;

View file

@ -1,5 +1,5 @@
---
import Layout from "../layouts/BaseLayout.astro";
import Layout from "@layouts/BaseLayout.astro";
---
<Layout>

View file

@ -1,6 +1,42 @@
---
import Layout from "../layouts/BaseLayout.astro";
import Layout from "@layouts/BaseLayout.astro";
import SmsClient from "@lib/SmsGatewayClient.ts";
export const prerender = false;
const errors = { name: "", phone: "", msg: "", form: "" };
let success = false;
if (Astro.request.method === "POST") {
try {
const data = await Astro.request.formData();
const name = data.get("name")?.toString();
const phone = data.get("phone")?.toString();
const msg = data.get("msg")?.toString();
if (typeof name !== "string" || name.length < 1) {
errors.name += "Please enter a name. ";
}
if (typeof phone !== "string") {
errors.phone += "Phone is not valid. ";
}
if (typeof msg !== "string" || msg.length < 20) {
errors.msg += "Message must be at least 20 characters. ";
}
const hasErrors = Object.values(errors).some(msg => msg)
if (!hasErrors) {
const smsClient = new SmsClient();
const message = "Web message from " + name + " (" + phone + "):\n\n" + msg;
const result = await smsClient.sendSMS(message);
if (!result.success) {
errors.form += "Sending SMS failed; API returned error. "
} else { success = true; }
}
} catch (error) {
if (error instanceof Error) {
errors.form += error.message;
}
}
}
---
<Layout>
@ -17,6 +53,9 @@ export const prerender = false;
"msg msg"
" . submit";
}
div {
grid-area: header;
}
label[for="name"] {
grid-area: name;
}
@ -32,21 +71,30 @@ export const prerender = false;
</style>
<Fragment slot="content">
<h2>Contact</h2>
<p>Use the below form to shoot me a quick text!</p>
<form x-data="{}">
{!success && <form method="post" x-data="{}">
<div>
<p>Use the below form to shoot me a quick text!</p>
{errors.form && <p>{errors.form}</p>}
</div>
<label for="name">
Name
<input type="text" id="name" placeholder="John Doe" />
<input type="text" id="name" name="name" placeholder="Bad Blocks" />
{errors.name && <p>{errors.name}</p>}
</label>
<label for="phone">
Phone
<input type="text" id="phone" placeholder="555-555-5555" />
<input type="text" id="phone" name="phone" placeholder="555-555-5555" />
{errors.phone && <p>{errors.phone}</p>}
</label>
<label for="msg">
Msg
<textarea id="msg" placeholder="I think badblocks rocks!"></textarea>
<textarea id="msg" name="msg" placeholder="I think badblocks rocks!"></textarea>
{errors.msg && <p>{errors.msg}</p>}
</label>
<label for="cap">
<cap-widget data-cap-api-endpoint="<your cap endpoint>"></cap-widget>
</label>
<button id="submit" type="submit">Submit</button>
</form>
</form> || <p>Your message has been sent successfully!</p>}
</Fragment>
</Layout>

View file

@ -1,5 +1,5 @@
---
import Layout from "../layouts/BaseLayout.astro";
import Layout from "@layouts/BaseLayout.astro";
---
<Layout>

View file

@ -1,5 +1,5 @@
---
import Layout from "../layouts/BaseLayout.astro";
import Layout from "@layouts/BaseLayout.astro";
---
<Layout>

View file

@ -1,5 +1,18 @@
{
"extends": "astro/tsconfigs/strict",
"extends": "astro/tsconfigs/strictest",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"exclude": ["dist"],
"compilerOptions": {
"verbatimModuleSyntax": true,
"paths": {
"@components/*": ["./src/components/*"],
"@layouts/*": ["./src/layouts/*"],
"@lib/*": ["./src/lib/*"],
},
"plugins": [
{
"name": "@astrojs/ts-plugin",
},
],
},
}