12 implement twilio sms (#37)
* add twilio sdk * add twilio config use app version in config * remove default config * phone regex * bun update * create client * fix env var * create Textarea component * move TextInput * allow snippets on labels * update with label and error * move button * make button children snippet * add form props * allow region code * add twilio FROM number * rename to twilioClient * implement simple messaging * add twilio phone number as empty var * format * move twilio client to local on requests * fix story * on second thought, dont use locals since we are only using twilio in one place Don't want to init a twilio client on every request when its only used in on a single page * use i18n for page text * validate env with a script * remove Zod validation when loading env vars
This commit is contained in:
parent
a4d998665b
commit
1c5b37b24b
29 changed files with 418 additions and 119 deletions
|
|
@ -1,2 +1,4 @@
|
|||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<a href="/app/sms" class="btn btn-ghost">SMS</a>
|
||||
|
|
|
|||
49
src/routes/app/sms/+page.server.ts
Normal file
49
src/routes/app/sms/+page.server.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { TWILIO_PHONE_NUMBER } from '$env/static/private';
|
||||
import { PhoneRegex } from '$lib/regex';
|
||||
import { logger } from '$lib/server/logger';
|
||||
import { TwilioClient } from '$lib/server/twilio';
|
||||
import { fail, type Actions } from '@sveltejs/kit';
|
||||
import zod from 'zod';
|
||||
|
||||
export const actions = {
|
||||
push: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
|
||||
if (!form.has('phone')) {
|
||||
return fail(400, { error: 'phone_missing' });
|
||||
}
|
||||
if (!form.get('message')) {
|
||||
return fail(400, { error: 'message_missing' });
|
||||
}
|
||||
|
||||
const {
|
||||
success: phoneSuccess,
|
||||
data: phone,
|
||||
error: phoneError,
|
||||
} = zod.string().regex(PhoneRegex).safeParse(form.get('phone'));
|
||||
if (!phoneSuccess) {
|
||||
logger.error(phoneError);
|
||||
return fail(400, { error: 'invalid_phone' });
|
||||
}
|
||||
|
||||
const message = form.get('message');
|
||||
if (typeof message !== 'string') {
|
||||
return fail(400, { error: 'invalid_message' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await TwilioClient.messages.create({
|
||||
to: phone,
|
||||
body: message,
|
||||
from: TWILIO_PHONE_NUMBER,
|
||||
});
|
||||
logger.debug(result);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
fail(500, { success: false });
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
} satisfies Actions;
|
||||
68
src/routes/app/sms/+page.svelte
Normal file
68
src/routes/app/sms/+page.svelte
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { Button } from '$lib/components/Actions';
|
||||
import { Textarea, TextInput } from '$lib/components/DataInput';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { ActionData } from './$types';
|
||||
import { messages } from '$lib/i18n';
|
||||
|
||||
type Props = {
|
||||
form: ActionData;
|
||||
};
|
||||
let { form }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#snippet PhoneLabel()}
|
||||
<i class="fi fi-sr-phone-flip"></i> {messages.sms_label_phone()}
|
||||
{/snippet}
|
||||
|
||||
{#snippet MessageLabel()}
|
||||
<i class="fi fi-sr-comment-alt"></i> {messages.sms_label_message()}
|
||||
{/snippet}
|
||||
|
||||
{#snippet alert(message: string)}
|
||||
<div role="alert" class="alert alert-error absolute -top-20" transition:fade>
|
||||
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="page" transition:fade>
|
||||
<div class="card bg-base-200 px-4 pt-4 shadow-xl">
|
||||
<div class="card-title justify-center">
|
||||
<h2 class="text-2xl font-semibold">{messages.sms_prompt()}</h2>
|
||||
{#if form?.error}
|
||||
{@render alert(form.error)}
|
||||
{/if}
|
||||
</div>
|
||||
<form id="sms" method="POST" action="?/push" use:enhance>
|
||||
<div class="card-body">
|
||||
<TextInput
|
||||
type="tel"
|
||||
name="phone"
|
||||
label={PhoneLabel}
|
||||
placeholder="XXX-XXX-XXXX"
|
||||
bordered
|
||||
fade
|
||||
/>
|
||||
<Textarea
|
||||
label={MessageLabel}
|
||||
size="lg"
|
||||
error={form?.error}
|
||||
name="message"
|
||||
placeholder="..."
|
||||
form="sms"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-actions justify-center px-8 pb-4">
|
||||
<Button outline type="submit" full>{messages.sms_button_submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-around gap-24 py-[10%];
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/common/Button';
|
||||
import TextInput from '$lib/components/common/TextInput';
|
||||
import { TextInput } from '$lib/components/DataInput';
|
||||
import { Button } from '$lib/components/Actions';
|
||||
import Tabs from '$lib/components/Navigation/Tabs';
|
||||
import { messages } from '$lib/i18n/index.js';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="card-actions px-4">
|
||||
<Button block type="submit" label={messages.login_button_submit()} outline />
|
||||
<Button block type="submit" outline>{messages.login_button_submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue