Merge branch 'master' into please-god-help-us
# Conflicts: # .env # bun.lockb # package.json # src/routes/login/+page.server.ts # src/routes/login/+page.svelte
This commit is contained in:
commit
4afc8a9efa
30 changed files with 435 additions and 124 deletions
9
.env
9
.env
|
|
@ -1,4 +1,11 @@
|
||||||
VITE_APP_VERSION=1.0.0-alpha
|
# TWILIO
|
||||||
|
TWILIO_ACCOUNT_SID=
|
||||||
|
TWILIO_AUTH_TOKEN=
|
||||||
|
TWILIO_PHONE_NUMBER=
|
||||||
|
|
||||||
|
# PRISMA
|
||||||
DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia"
|
DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia"
|
||||||
|
|
||||||
|
# CLERK
|
||||||
PUBLIC_CLERK_PUBLISHABLE_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead
|
PUBLIC_CLERK_PUBLISHABLE_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead
|
||||||
CLERK_SECRET_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead
|
CLERK_SECRET_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead
|
||||||
|
|
|
||||||
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
bunx prettier . --write
|
||||||
|
bunx eslint_d $(git diff --name-only HEAD | grep -E '\.(*)$' | xargs)
|
||||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -1,11 +1,21 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"greeting": "Hello {name}!",
|
"nav_greeting": "Hello {name}!",
|
||||||
"email_required": "Email is required",
|
"login_tab_login": "Login",
|
||||||
"password_required": "Password is required",
|
"login_tab_register": "Register",
|
||||||
"name_required": "Name is required",
|
"login_label_email": "Email",
|
||||||
"email_incorrect": "Email is incorrect",
|
"login_label_password": "Password",
|
||||||
"password_incorrect": "Password is incorrect",
|
"login_label_name": "Name",
|
||||||
"email_inuse": "Email is already in use",
|
"login_button_submit": "Submit",
|
||||||
"user_not_found": "The user could not be found"
|
"login_error_email_required": "Email is required",
|
||||||
|
"login_error_password_required": "Password is required",
|
||||||
|
"login_error_name_required": "Name is required",
|
||||||
|
"login_error_email_incorrect": "Email is incorrect",
|
||||||
|
"login_error_password_incorrect": "Password is incorrect",
|
||||||
|
"login_error_email_inuse": "Email is already in use",
|
||||||
|
"login_error_user_not_found": "The user could not be found",
|
||||||
|
"sms_prompt": "Send a Message",
|
||||||
|
"sms_label_phone": "Phone Number",
|
||||||
|
"sms_label_message": "Message",
|
||||||
|
"sms_button_submit": "Send Message"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun database:up && bun prisma:generate && bun prisma:push && vite dev",
|
"dev": "bun validate-env && bun database:up && bun prisma:generate && bun prisma:push && vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432",
|
"database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432",
|
||||||
|
|
@ -23,7 +23,9 @@
|
||||||
"prisma:push": "prisma db push",
|
"prisma:push": "prisma db push",
|
||||||
"prisma:reset": "prisma migrate reset --force",
|
"prisma:reset": "prisma migrate reset --force",
|
||||||
"prisma:studio": "prisma studio",
|
"prisma:studio": "prisma studio",
|
||||||
"prisma:validate": "prisma validate"
|
"prisma:validate": "prisma validate",
|
||||||
|
"prepare": "husky",
|
||||||
|
"validate-env": "bun ./scripts/validate-env.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^3.2.2",
|
"@chromatic-com/storybook": "^3.2.2",
|
||||||
|
|
@ -48,6 +50,7 @@
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
|
@ -73,12 +76,13 @@
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"clerk-sveltekit": "https://pkg.pr.new/wobsoriano/clerk-sveltekit@ca15d4e",
|
"clerk-sveltekit": "https://pkg.pr.new/wobsoriano/clerk-sveltekit@ca15d4e",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"eslint_d": "^14.3.0",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-yoga": "^5.10.4",
|
"graphql-yoga": "^5.10.4",
|
||||||
"oslo": "^1.2.1",
|
|
||||||
"pino": "^9.5.0",
|
"pino": "^9.5.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
"twilio": "^5.4.0",
|
||||||
"zod": "^3.24.0"
|
"zod": "^3.24.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
scripts/validate-env.ts
Normal file
19
scripts/validate-env.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { PhoneRegex } from '../src/lib/regex/phone';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const ValidateEnvironment = () => {
|
||||||
|
const { success, error } = z
|
||||||
|
.object({
|
||||||
|
TWILIO_ACCOUNT_SID: z.string().min(1),
|
||||||
|
TWILIO_AUTH_TOKEN: z.string().min(1),
|
||||||
|
TWILIO_PHONE_NUMBER: z.string().regex(PhoneRegex),
|
||||||
|
})
|
||||||
|
.safeParse(process.env);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.error(error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ValidateEnvironment();
|
||||||
1
src/app.d.ts
vendored
1
src/app.d.ts
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
import Button from './Button.svelte';
|
|
||||||
import { fn } from '@storybook/test';
|
import { fn } from '@storybook/test';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
title: 'Actions/Button',
|
title: 'Actions/Button',
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
onClick: fn(),
|
onClick: fn(),
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
block: { control: 'boolean' },
|
||||||
color: {
|
color: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: [
|
options: [
|
||||||
|
|
@ -25,9 +27,12 @@
|
||||||
'error',
|
'error',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
full: { control: 'boolean' },
|
||||||
|
glass: { control: 'boolean' },
|
||||||
outline: {
|
outline: {
|
||||||
control: 'boolean',
|
control: 'boolean',
|
||||||
},
|
},
|
||||||
|
responsive: { control: 'boolean' },
|
||||||
size: {
|
size: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: ['Default', 'xs', 'sm', 'lg'],
|
options: ['Default', 'xs', 'sm', 'lg'],
|
||||||
|
|
@ -37,8 +42,13 @@
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: ['button', 'reset', 'submit'],
|
options: ['button', 'reset', 'submit'],
|
||||||
},
|
},
|
||||||
|
wide: { control: 'boolean' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Story name="Default" args={{ label: 'Button', color: 'primary' }} />
|
{#snippet template({ children: _, ...props }: Partial<ComponentProps<typeof Button>>)}
|
||||||
|
<Button {...props}>Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Story name="Default" args={{}} children={template} />
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { DaisyColor, DaisySize } from '$lib/types';
|
import type { DaisyColor, DaisySize } from '$lib/types';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
|
children: Snippet;
|
||||||
color?: DaisyColor;
|
color?: DaisyColor;
|
||||||
|
full?: boolean;
|
||||||
glass?: boolean;
|
glass?: boolean;
|
||||||
label: string;
|
|
||||||
outline?: boolean;
|
outline?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
responsive?: boolean;
|
responsive?: boolean;
|
||||||
|
|
@ -17,9 +19,10 @@
|
||||||
|
|
||||||
let {
|
let {
|
||||||
block = false,
|
block = false,
|
||||||
|
children,
|
||||||
color,
|
color,
|
||||||
|
full = false,
|
||||||
glass = false,
|
glass = false,
|
||||||
label,
|
|
||||||
outline = false,
|
outline = false,
|
||||||
onClick,
|
onClick,
|
||||||
responsive = false,
|
responsive = false,
|
||||||
|
|
@ -35,6 +38,7 @@
|
||||||
class:btn-outline={outline}
|
class:btn-outline={outline}
|
||||||
class:btn-block={block}
|
class:btn-block={block}
|
||||||
class:btn-wide={wide}
|
class:btn-wide={wide}
|
||||||
|
class:w-full={full}
|
||||||
class:glass
|
class:glass
|
||||||
class:btn-xs={size === 'xs'}
|
class:btn-xs={size === 'xs'}
|
||||||
class:btn-sm={size === 'sm'}
|
class:btn-sm={size === 'sm'}
|
||||||
|
|
@ -51,7 +55,7 @@
|
||||||
class:btn-error={color === 'error'}
|
class:btn-error={color === 'error'}
|
||||||
class={`btn ${responsive && 'btn-xs sm:btn-sm md:btn-md lg:btn-lg'}`}
|
class={`btn ${responsive && 'btn-xs sm:btn-sm md:btn-md lg:btn-lg'}`}
|
||||||
>
|
>
|
||||||
{label}
|
{@render children()}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
1
src/lib/components/Actions/index.ts
Normal file
1
src/lib/components/Actions/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Button } from './Button.svelte';
|
||||||
|
|
@ -6,27 +6,36 @@
|
||||||
title: 'Data Input/Text Input',
|
title: 'Data Input/Text Input',
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
bordered: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
color: {
|
color: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: [
|
options: [
|
||||||
|
'ghost',
|
||||||
'primary',
|
'primary',
|
||||||
'secondary',
|
'secondary',
|
||||||
'accent',
|
'accent',
|
||||||
'ghost',
|
|
||||||
'link',
|
|
||||||
'info',
|
'info',
|
||||||
'success',
|
'success',
|
||||||
'warning',
|
'warning',
|
||||||
'error',
|
'error',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
bordered: {
|
disabled: {
|
||||||
control: 'boolean',
|
control: 'boolean',
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
fade: { control: 'boolean' },
|
||||||
|
start: { control: 'text' },
|
||||||
|
end: { control: 'text' },
|
||||||
|
label: { control: 'text' },
|
||||||
|
placeholder: { control: 'text' },
|
||||||
size: {
|
size: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: ['Default', 'xs', 'sm', 'lg'],
|
options: ['xs', 'sm', '-', 'lg'],
|
||||||
defaultValue: 'Default',
|
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
99
src/lib/components/DataInput/TextInput.svelte
Normal file
99
src/lib/components/DataInput/TextInput.svelte
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DaisyColor, DaisySize } from '$lib/types';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { HTMLInputTypeAttribute } from 'svelte/elements';
|
||||||
|
import { fade as fadeTransition } from 'svelte/transition';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
bordered?: boolean;
|
||||||
|
color?: Exclude<DaisyColor, 'neutral'>;
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: string | Snippet;
|
||||||
|
fade?: boolean;
|
||||||
|
start?: string | Snippet;
|
||||||
|
end?: string | Snippet;
|
||||||
|
label?: string | Snippet;
|
||||||
|
name: string;
|
||||||
|
placeholder?: string;
|
||||||
|
size?: DaisySize;
|
||||||
|
type?: Extract<
|
||||||
|
HTMLInputTypeAttribute,
|
||||||
|
'email' | 'password' | 'search' | 'tel' | 'text' | 'url'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
bordered = false,
|
||||||
|
color,
|
||||||
|
disabled,
|
||||||
|
error,
|
||||||
|
fade,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
placeholder,
|
||||||
|
size,
|
||||||
|
type = 'text',
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text"
|
||||||
|
class:text-primary={color === 'primary'}
|
||||||
|
class:text-secondary={color === 'secondary'}
|
||||||
|
class:text-accent={color === 'accent'}
|
||||||
|
class:text-info={color === 'info'}
|
||||||
|
class:text-success={color === 'success'}
|
||||||
|
class:text-warning={color === 'warning'}
|
||||||
|
class:text-error={color === 'error' || error}
|
||||||
|
>
|
||||||
|
{#if typeof label === 'string'}
|
||||||
|
{label}
|
||||||
|
{:else if label}
|
||||||
|
{@render label()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="label-text-alt font-semibold text-error">
|
||||||
|
{#if typeof error === 'string'}
|
||||||
|
{error}
|
||||||
|
{:else if error}
|
||||||
|
{@render error()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
transition:fadeTransition={{ duration: fade ? 200 : 0 }}
|
||||||
|
class="input flex w-full items-center gap-2"
|
||||||
|
class:input-bordered={bordered}
|
||||||
|
class:input-xs={size === 'xs'}
|
||||||
|
class:input-sm={size === 'sm'}
|
||||||
|
class:input-lg={size === 'lg'}
|
||||||
|
class:input-primary={color === 'primary'}
|
||||||
|
class:input-secondary={color === 'secondary'}
|
||||||
|
class:input-accent={color === 'accent'}
|
||||||
|
class:input-ghost={color === 'ghost'}
|
||||||
|
class:input-link={color === 'link'}
|
||||||
|
class:input-info={color === 'info'}
|
||||||
|
class:input-success={color === 'success'}
|
||||||
|
class:input-warning={color === 'warning'}
|
||||||
|
class:input-error={color === 'error' || error}
|
||||||
|
>
|
||||||
|
{#if typeof start === 'string'}
|
||||||
|
{start}
|
||||||
|
{:else}
|
||||||
|
{@render start?.()}
|
||||||
|
{/if}
|
||||||
|
<input {disabled} {name} {placeholder} {type} class="grow" />
|
||||||
|
{#if typeof end === 'string'}
|
||||||
|
{end}
|
||||||
|
{:else}
|
||||||
|
{@render end?.()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
51
src/lib/components/DataInput/Textarea.stories.svelte
Normal file
51
src/lib/components/DataInput/Textarea.stories.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import Textarea from './Textarea.svelte';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Data Input/Textarea',
|
||||||
|
component: Textarea,
|
||||||
|
argTypes: {
|
||||||
|
bordered: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
'default',
|
||||||
|
'ghost',
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'accent',
|
||||||
|
'info',
|
||||||
|
'success',
|
||||||
|
'warning',
|
||||||
|
'error',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['xs', 'sm', '-', 'lg'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet template(props: ComponentProps<typeof Textarea>)}
|
||||||
|
<Textarea {...props} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Story name="Default" args={{ label: 'Label' }} children={template} />
|
||||||
61
src/lib/components/DataInput/Textarea.svelte
Normal file
61
src/lib/components/DataInput/Textarea.svelte
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DaisyColor, DaisySize } from '$lib/types';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
bordered?: boolean;
|
||||||
|
color?: Omit<DaisyColor, 'neutral'>;
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: string | Snippet;
|
||||||
|
form?: string;
|
||||||
|
label?: string | Snippet;
|
||||||
|
name?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
size?: DaisySize;
|
||||||
|
};
|
||||||
|
let { bordered, color, error, label, size, ...props }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="form-control w-full max-w-lg">
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text"
|
||||||
|
class:text-primary={color === 'primary'}
|
||||||
|
class:text-secondary={color === 'secondary'}
|
||||||
|
class:text-accent={color === 'accent'}
|
||||||
|
class:text-info={color === 'info'}
|
||||||
|
class:text-success={color === 'success'}
|
||||||
|
class:text-warning={color === 'warning'}
|
||||||
|
class:text-error={color === 'error' || error}
|
||||||
|
>
|
||||||
|
{#if typeof label === 'string'}
|
||||||
|
{label}
|
||||||
|
{:else if label}
|
||||||
|
{@render label()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="label-text-alt font-semibold text-error">
|
||||||
|
{#if typeof error === 'string'}
|
||||||
|
{error}
|
||||||
|
{:else if error}
|
||||||
|
{@render error()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
class:textarea-bordered={bordered}
|
||||||
|
class:textarea-xs={size === 'xs'}
|
||||||
|
class:textarea-sm={size === 'sm'}
|
||||||
|
class:textarea-lg={size === 'lg'}
|
||||||
|
class:textarea-ghost={color === 'ghost'}
|
||||||
|
class:textarea-primary={color === 'primary'}
|
||||||
|
class:textarea-secondary={color === 'secondary'}
|
||||||
|
class:textarea-accent={color === 'accent'}
|
||||||
|
class:textarea-info={color === 'info'}
|
||||||
|
class:textarea-success={color === 'success'}
|
||||||
|
class:textarea-warning={color === 'warning'}
|
||||||
|
class:textarea-error={color === 'error' || error}
|
||||||
|
{...props}
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
2
src/lib/components/DataInput/index.ts
Normal file
2
src/lib/components/DataInput/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Textarea } from './Textarea.svelte';
|
||||||
|
export { default as TextInput } from './TextInput.svelte';
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
let { title, username }: { title: string; username: string } = $props();
|
let { title, username }: { title: string; username: string } = $props();
|
||||||
|
|
||||||
let message = $derived(messages.greeting({ name: username }));
|
let message = $derived(messages.nav_greeting({ name: username }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="navbar justify-between bg-base-200 px-4">
|
<header class="navbar justify-between bg-base-200 px-4">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import Button from './Button.svelte';
|
|
||||||
|
|
||||||
export default Button;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { DaisyColor, DaisySize } from '$lib/types';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import type { HTMLInputTypeAttribute } from 'svelte/elements';
|
|
||||||
import { fade as fadeTransition } from 'svelte/transition';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
bordered?: boolean;
|
|
||||||
color?: Exclude<DaisyColor, 'neutral'>;
|
|
||||||
disabled?: boolean;
|
|
||||||
fade?: boolean;
|
|
||||||
start?: Snippet | string;
|
|
||||||
end?: Snippet | string;
|
|
||||||
name: string;
|
|
||||||
placeholder?: string;
|
|
||||||
size?: DaisySize;
|
|
||||||
type?: Extract<
|
|
||||||
HTMLInputTypeAttribute,
|
|
||||||
'email' | 'password' | 'search' | 'tel' | 'text' | 'url'
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
let {
|
|
||||||
bordered = false,
|
|
||||||
color,
|
|
||||||
disabled,
|
|
||||||
fade,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
name,
|
|
||||||
placeholder,
|
|
||||||
size,
|
|
||||||
type = 'text',
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<label
|
|
||||||
transition:fadeTransition={{ duration: fade ? 200 : 0 }}
|
|
||||||
class="input flex w-full items-center gap-2"
|
|
||||||
class:input-bordered={bordered}
|
|
||||||
class:input-xs={size === 'xs'}
|
|
||||||
class:input-sm={size === 'sm'}
|
|
||||||
class:input-lg={size === 'lg'}
|
|
||||||
class:input-primary={color === 'primary'}
|
|
||||||
class:input-secondary={color === 'secondary'}
|
|
||||||
class:input-accent={color === 'accent'}
|
|
||||||
class:input-ghost={color === 'ghost'}
|
|
||||||
class:input-link={color === 'link'}
|
|
||||||
class:input-info={color === 'info'}
|
|
||||||
class:input-success={color === 'success'}
|
|
||||||
class:input-warning={color === 'warning'}
|
|
||||||
class:input-error={color === 'error'}
|
|
||||||
>
|
|
||||||
{#if typeof start === 'string'}
|
|
||||||
{start}
|
|
||||||
{:else}
|
|
||||||
{@render start?.()}
|
|
||||||
{/if}
|
|
||||||
<input {disabled} {name} {placeholder} {type} class="grow" />
|
|
||||||
{#if typeof end === 'string'}
|
|
||||||
{end}
|
|
||||||
{:else}
|
|
||||||
{@render end?.()}
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import TextInput from './TextInput.svelte';
|
|
||||||
|
|
||||||
export default TextInput;
|
|
||||||
1
src/lib/regex/index.ts
Normal file
1
src/lib/regex/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './phone';
|
||||||
1
src/lib/regex/phone.ts
Normal file
1
src/lib/regex/phone.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const PhoneRegex = /^\+?\d?(\d{3}\d{3}\d{4}|\d{3}-\d{3}-\d{4})$/;
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { logger } from '$lib/server/logger';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export interface Configuration {
|
|
||||||
app_version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoadConfig = (): Configuration => {
|
|
||||||
const { success, data, error } = z
|
|
||||||
.object({
|
|
||||||
VITE_APP_VERSION: z.string().default('development'),
|
|
||||||
})
|
|
||||||
.safeParse(import.meta.env);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
logger.error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
app_version: data!.VITE_APP_VERSION,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Config = LoadConfig();
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { version } from '$app/environment';
|
||||||
import { builder } from '../builder';
|
import { builder } from '../builder';
|
||||||
|
|
||||||
builder.queryType({});
|
builder.queryType({});
|
||||||
|
|
@ -5,7 +6,7 @@ builder.queryType({});
|
||||||
builder.queryField('version', (t) =>
|
builder.queryField('version', (t) =>
|
||||||
t.string({
|
t.string({
|
||||||
description: 'Application version',
|
description: 'Application version',
|
||||||
resolve: (parent, args, context) => context.config.app_version,
|
resolve: () => version,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
4
src/lib/server/twilio/client.ts
Normal file
4
src/lib/server/twilio/client.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN } from '$env/static/private';
|
||||||
|
import twilio from 'twilio';
|
||||||
|
|
||||||
|
export const TwilioClient = twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
|
||||||
1
src/lib/server/twilio/index.ts
Normal file
1
src/lib/server/twilio/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './client';
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Config } from '$lib/server/config';
|
|
||||||
import type { YogaInitialContext } from 'graphql-yoga';
|
import type { YogaInitialContext } from 'graphql-yoga';
|
||||||
|
|
||||||
export const Context = (initialContext: YogaInitialContext) => ({
|
export const Context = (initialContext: YogaInitialContext) => ({
|
||||||
...initialContext,
|
...initialContext,
|
||||||
config: Config,
|
config: {},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
</script>
|
</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>
|
||||||
|
|
@ -8,6 +8,9 @@ const config = {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
|
version: {
|
||||||
|
name: '1.0.0-alpha',
|
||||||
|
},
|
||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue