work on porting over contact form from old site, also added initial db support
to use later
This commit is contained in:
parent
8d989ef36f
commit
f641dac69b
12 changed files with 232 additions and 32 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -13,9 +13,8 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
# environment variables
|
# prod environment variables
|
||||||
.env
|
.env
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,52 @@
|
||||||
import htmx from "astro-htmx";
|
import htmx from "astro-htmx";
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from "astro/config";
|
import { defineConfig, envField } from "astro/config";
|
||||||
import alpinejs from "@astrojs/alpinejs";
|
import alpinejs from "@astrojs/alpinejs";
|
||||||
import partytown from "@astrojs/partytown";
|
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
import bun from "@nurodev/astro-bun";
|
import bun from "@nurodev/astro-bun";
|
||||||
|
import db from "@astrojs/db";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://badblocks.dev",
|
site: "https://badblocks.dev",
|
||||||
|
trailingSlash: "never",
|
||||||
adapter: bun(),
|
adapter: bun(),
|
||||||
output: "static",
|
output: "static",
|
||||||
integrations: [
|
devToolbar: { enabled: false },
|
||||||
alpinejs(),
|
prefetch: {
|
||||||
partytown(),
|
prefetchAll: true,
|
||||||
sitemap(),
|
},
|
||||||
htmx(),
|
security: {
|
||||||
// sitemap({
|
checkOrigin: true,
|
||||||
// filter: (page) =>
|
},
|
||||||
// page !== "https://example.com/secret-vip-lounge-1/" &&
|
server: {
|
||||||
// page !== "https://example.com/secret-vip-lounge-2/",
|
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
BIN
bun.lockb
Binary file not shown.
14
package.json
14
package.json
|
|
@ -5,20 +5,30 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "bun run ./dist/server/entry.mjs",
|
"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",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/alpinejs": "^0.4.9",
|
"@astrojs/alpinejs": "^0.4.9",
|
||||||
|
"@astrojs/check": "^0.9.6",
|
||||||
|
"@astrojs/db": "^0.18.3",
|
||||||
"@astrojs/partytown": "^2.1.4",
|
"@astrojs/partytown": "^2.1.4",
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@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",
|
"@nurodev/astro-bun": "^2.1.2",
|
||||||
"@types/alpinejs": "^3.13.11",
|
"@types/alpinejs": "^3.13.11",
|
||||||
"alpinejs": "^3.15.3",
|
"alpinejs": "^3.15.3",
|
||||||
|
"android-sms-gateway": "^3.0.0",
|
||||||
"astro": "^5.16.6",
|
"astro": "^5.16.6",
|
||||||
"astro-htmx": "^1.0.6",
|
"astro-htmx": "^1.0.6",
|
||||||
"htmx.org": "^2.0.8"
|
"htmx.org": "^2.0.8",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.5"
|
"@types/bun": "^1.3.5"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@
|
||||||
href="https://fonts.googleapis.com/css2?family=VT323&display=swap"
|
href="https://fonts.googleapis.com/css2?family=VT323&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<script>
|
||||||
|
const yearSpan = document.querySelector("#copyright-year");
|
||||||
|
if (yearSpan) {
|
||||||
|
yearSpan.innerHTML = new Date().getFullYear().toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--min-body-width: 30ch;
|
--min-body-width: 30ch;
|
||||||
|
|
@ -96,9 +102,7 @@
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<p>
|
<p>
|
||||||
© <script is:inline>
|
© <span id="copyright-year">2026</span>
|
||||||
document.write(new Date().getFullYear());
|
|
||||||
</script>
|
|
||||||
badblocks
|
badblocks
|
||||||
</p>
|
</p>
|
||||||
<p>Made from scratch with BAHA: Bun, Astro, Htmx, and Alpine!</p>
|
<p>Made from scratch with BAHA: Bun, Astro, Htmx, and Alpine!</p>
|
||||||
|
|
|
||||||
46
src/lib/HttpFetchClient.ts
Normal file
46
src/lib/HttpFetchClient.ts
Normal 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;
|
||||||
53
src/lib/SmsGatewayClient.ts
Normal file
53
src/lib/SmsGatewayClient.ts
Normal 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;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from "../layouts/BaseLayout.astro";
|
import Layout from "@layouts/BaseLayout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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>
|
<Layout>
|
||||||
|
|
@ -17,6 +53,9 @@ export const prerender = false;
|
||||||
"msg msg"
|
"msg msg"
|
||||||
" . submit";
|
" . submit";
|
||||||
}
|
}
|
||||||
|
div {
|
||||||
|
grid-area: header;
|
||||||
|
}
|
||||||
label[for="name"] {
|
label[for="name"] {
|
||||||
grid-area: name;
|
grid-area: name;
|
||||||
}
|
}
|
||||||
|
|
@ -32,21 +71,30 @@ export const prerender = false;
|
||||||
</style>
|
</style>
|
||||||
<Fragment slot="content">
|
<Fragment slot="content">
|
||||||
<h2>Contact</h2>
|
<h2>Contact</h2>
|
||||||
<p>Use the below form to shoot me a quick text!</p>
|
{!success && <form method="post" x-data="{}">
|
||||||
<form 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">
|
<label for="name">
|
||||||
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>
|
||||||
<label for="phone">
|
<label for="phone">
|
||||||
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>
|
||||||
<label for="msg">
|
<label for="msg">
|
||||||
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>
|
</label>
|
||||||
<button id="submit" type="submit">Submit</button>
|
<button id="submit" type="submit">Submit</button>
|
||||||
</form>
|
</form> || <p>Your message has been sent successfully!</p>}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from "../layouts/BaseLayout.astro";
|
import Layout from "@layouts/BaseLayout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from "../layouts/BaseLayout.astro";
|
import Layout from "@layouts/BaseLayout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strictest",
|
||||||
"include": [".astro/types.d.ts", "**/*"],
|
"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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue