@webamoki/web-svelte 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/form/IconInputWrapper.svelte +39 -0
- package/dist/components/form/IconInputWrapper.svelte.d.ts +20 -0
- package/dist/components/form/fields/DateField.svelte +12 -4
- package/dist/components/form/fields/DateField.svelte.d.ts +3 -1
- package/dist/components/form/fields/MessageField.svelte +19 -10
- package/dist/components/form/fields/MessageField.svelte.d.ts +2 -0
- package/dist/components/form/fields/NumberField.svelte +26 -3
- package/dist/components/form/fields/NumberField.svelte.d.ts +3 -0
- package/dist/components/form/fields/PasswordField.svelte +14 -5
- package/dist/components/form/fields/PasswordField.svelte.d.ts +3 -0
- package/dist/components/form/fields/SelectField.svelte +13 -5
- package/dist/components/form/fields/SelectField.svelte.d.ts +2 -0
- package/dist/components/form/fields/SelectMultiField.svelte +13 -5
- package/dist/components/form/fields/SelectMultiField.svelte.d.ts +2 -0
- package/dist/components/form/fields/TextField.svelte +11 -2
- package/dist/components/form/fields/TextField.svelte.d.ts +2 -0
- package/dist/components/form/fields/TextFieldNullable.svelte +22 -2
- package/dist/components/form/fields/TextFieldNullable.svelte.d.ts +2 -0
- package/dist/components/form/fields/TimeField.svelte +27 -4
- package/dist/components/form/fields/TimeField.svelte.d.ts +4 -1
- package/dist/components/form/index.d.ts +3 -2
- package/dist/components/form/index.js +3 -2
- package/dist/server/form-handler.js +7 -6
- package/dist/utils/email/README.md +12 -2
- package/dist/utils/email/aws-signer.d.ts +17 -0
- package/dist/utils/email/aws-signer.js +86 -0
- package/dist/utils/email/ses.d.ts +2 -1
- package/dist/utils/email/ses.js +92 -37
- package/package.json +6 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
icon?: Component;
|
|
6
|
+
/**
|
|
7
|
+
* Position of the icon vertically.
|
|
8
|
+
* - 'center': Centers vertically (for single-line inputs)
|
|
9
|
+
* - 'top': Aligns to top with padding (for multi-line inputs like textarea)
|
|
10
|
+
*/
|
|
11
|
+
iconPosition?: 'center' | 'top';
|
|
12
|
+
/**
|
|
13
|
+
* Whether this wrapper should grow to fill flex container (used in layouts with buttons)
|
|
14
|
+
*/
|
|
15
|
+
flex?: boolean;
|
|
16
|
+
children: Snippet<[{ class: string }]>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { icon, iconPosition = 'center', flex = false, children }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const iconClass =
|
|
22
|
+
iconPosition === 'top'
|
|
23
|
+
? 'pointer-events-none absolute top-4 left-4 text-gray-500'
|
|
24
|
+
: 'pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-gray-500';
|
|
25
|
+
|
|
26
|
+
const wrapperClass = flex ? 'relative flex-1' : 'relative';
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if icon}
|
|
30
|
+
{@const Icon = icon}
|
|
31
|
+
<div class={wrapperClass}>
|
|
32
|
+
<div class={iconClass}>
|
|
33
|
+
<Icon class="size-5" />
|
|
34
|
+
</div>
|
|
35
|
+
{@render children({ class: 'pl-12' })}
|
|
36
|
+
</div>
|
|
37
|
+
{:else}
|
|
38
|
+
{@render children({ class: '' })}
|
|
39
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
icon?: Component;
|
|
4
|
+
/**
|
|
5
|
+
* Position of the icon vertically.
|
|
6
|
+
* - 'center': Centers vertically (for single-line inputs)
|
|
7
|
+
* - 'top': Aligns to top with padding (for multi-line inputs like textarea)
|
|
8
|
+
*/
|
|
9
|
+
iconPosition?: 'center' | 'top';
|
|
10
|
+
/**
|
|
11
|
+
* Whether this wrapper should grow to fill flex container (used in layouts with buttons)
|
|
12
|
+
*/
|
|
13
|
+
flex?: boolean;
|
|
14
|
+
children: Snippet<[{
|
|
15
|
+
class: string;
|
|
16
|
+
}]>;
|
|
17
|
+
}
|
|
18
|
+
declare const IconInputWrapper: Component<Props, {}, "">;
|
|
19
|
+
type IconInputWrapper = ReturnType<typeof IconInputWrapper>;
|
|
20
|
+
export default IconInputWrapper;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
-
import
|
|
3
|
-
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
2
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
4
3
|
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
4
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
5
5
|
import { CalendarDate } from '@internationalized/date';
|
|
6
|
+
import type { Component } from 'svelte';
|
|
7
|
+
import type { FormPath } from 'sveltekit-superforms';
|
|
8
|
+
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
6
9
|
|
|
7
10
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
8
11
|
value?: CalendarDate;
|
|
9
12
|
class?: string;
|
|
13
|
+
icon?: Component;
|
|
10
14
|
}
|
|
11
|
-
let { value = $bindable(), class: className, ...fieldProps }: Props = $props();
|
|
15
|
+
let { value = $bindable(), class: className, icon, ...fieldProps }: Props = $props();
|
|
12
16
|
|
|
13
17
|
// Getter: format CalendarDate → string (YYYY-MM-DD)
|
|
14
18
|
function get(): string {
|
|
@@ -34,6 +38,10 @@
|
|
|
34
38
|
|
|
35
39
|
<FieldWrapper {...fieldProps}>
|
|
36
40
|
{#snippet formElem(props)}
|
|
37
|
-
<
|
|
41
|
+
<IconInputWrapper {icon}>
|
|
42
|
+
{#snippet children({ class: iconClass })}
|
|
43
|
+
<Input type="date" bind:value={get, set} class={cn(iconClass, className)} {...props} />
|
|
44
|
+
{/snippet}
|
|
45
|
+
</IconInputWrapper>
|
|
38
46
|
{/snippet}
|
|
39
47
|
</FieldWrapper>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { CalendarDate } from '@internationalized/date';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
1
3
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
4
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
|
-
import { CalendarDate } from '@internationalized/date';
|
|
4
5
|
declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
5
6
|
props: FieldWrapperProps<T, U, M> & {
|
|
6
7
|
value?: CalendarDate;
|
|
7
8
|
class?: string;
|
|
9
|
+
icon?: Component;
|
|
8
10
|
};
|
|
9
11
|
exports: {};
|
|
10
12
|
bindings: "value";
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
-
import
|
|
3
|
-
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
2
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
4
3
|
import { Textarea } from '../../../shadcn/components/ui/textarea/index.js';
|
|
4
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
5
5
|
import { Lock, LockOpen } from '@lucide/svelte';
|
|
6
|
+
import type { Component } from 'svelte';
|
|
7
|
+
import type { FormPath } from 'sveltekit-superforms';
|
|
8
|
+
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
6
9
|
|
|
7
10
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
8
11
|
value?: string;
|
|
@@ -11,6 +14,7 @@
|
|
|
11
14
|
defaultHeight?: number;
|
|
12
15
|
showLock?: boolean;
|
|
13
16
|
defaultLocked?: boolean;
|
|
17
|
+
icon?: Component;
|
|
14
18
|
}
|
|
15
19
|
let {
|
|
16
20
|
value = $bindable(),
|
|
@@ -19,6 +23,7 @@
|
|
|
19
23
|
defaultHeight = 100,
|
|
20
24
|
showLock = true,
|
|
21
25
|
defaultLocked = false,
|
|
26
|
+
icon,
|
|
22
27
|
...fieldProps
|
|
23
28
|
}: Props = $props();
|
|
24
29
|
|
|
@@ -28,14 +33,18 @@
|
|
|
28
33
|
<FieldWrapper {...fieldProps}>
|
|
29
34
|
{#snippet formElem(props)}
|
|
30
35
|
<div class="flex w-full items-start gap-2">
|
|
31
|
-
<!-- Textarea itself -->
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
<!-- Textarea itself with optional left icon -->
|
|
37
|
+
<IconInputWrapper {icon} iconPosition="top" flex>
|
|
38
|
+
{#snippet children({ class: iconClass })}
|
|
39
|
+
<Textarea
|
|
40
|
+
bind:value
|
|
41
|
+
class={cn(iconClass, className || '', locked ? 'resize-none' : 'resize-y')}
|
|
42
|
+
{placeholder}
|
|
43
|
+
style={defaultHeight ? `height: ${defaultHeight}px` : undefined}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
{/snippet}
|
|
47
|
+
</IconInputWrapper>
|
|
39
48
|
|
|
40
49
|
<!-- Lock/unlock button -->
|
|
41
50
|
{#if showLock}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
@@ -8,6 +9,7 @@ declare function $$render<T extends Record<string, unknown>, U extends FormPath<
|
|
|
8
9
|
defaultHeight?: number;
|
|
9
10
|
showLock?: boolean;
|
|
10
11
|
defaultLocked?: boolean;
|
|
12
|
+
icon?: Component;
|
|
11
13
|
};
|
|
12
14
|
exports: {};
|
|
13
15
|
bindings: "value";
|
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
<script lang="ts" generics="V,T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
3
|
+
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
4
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
5
|
+
import type { Component } from 'svelte';
|
|
2
6
|
import type { FormPath } from 'sveltekit-superforms';
|
|
3
7
|
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
4
|
-
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
5
8
|
|
|
6
9
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
7
10
|
value?: V;
|
|
8
11
|
class?: string;
|
|
9
12
|
step?: HTMLInputElement['step'];
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
icon?: Component;
|
|
10
15
|
}
|
|
11
|
-
let {
|
|
16
|
+
let {
|
|
17
|
+
value = $bindable(),
|
|
18
|
+
class: className,
|
|
19
|
+
step,
|
|
20
|
+
placeholder,
|
|
21
|
+
icon,
|
|
22
|
+
...fieldProps
|
|
23
|
+
}: Props = $props();
|
|
12
24
|
</script>
|
|
13
25
|
|
|
14
26
|
<FieldWrapper {...fieldProps}>
|
|
15
27
|
{#snippet formElem(props)}
|
|
16
|
-
<
|
|
28
|
+
<IconInputWrapper {icon}>
|
|
29
|
+
{#snippet children({ class: iconClass })}
|
|
30
|
+
<Input
|
|
31
|
+
type="number"
|
|
32
|
+
bind:value
|
|
33
|
+
class={cn(iconClass, className)}
|
|
34
|
+
{placeholder}
|
|
35
|
+
{step}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
{/snippet}
|
|
39
|
+
</IconInputWrapper>
|
|
17
40
|
{/snippet}
|
|
18
41
|
</FieldWrapper>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<V, T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
@@ -5,6 +6,8 @@ declare function $$render<V, T extends Record<string, unknown>, U extends FormPa
|
|
|
5
6
|
value?: V;
|
|
6
7
|
class?: string;
|
|
7
8
|
step?: HTMLInputElement["step"];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
icon?: Component;
|
|
8
11
|
};
|
|
9
12
|
exports: {};
|
|
10
13
|
bindings: "value";
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
-
import
|
|
3
|
-
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
2
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
4
3
|
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
4
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
5
5
|
import { Eye, EyeOff } from '@lucide/svelte';
|
|
6
|
+
import type { Component } from 'svelte';
|
|
7
|
+
import type { FormPath } from 'sveltekit-superforms';
|
|
8
|
+
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
6
9
|
|
|
7
10
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
8
11
|
value?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
icon?: Component;
|
|
9
14
|
}
|
|
10
|
-
let { value = $bindable(), ...fieldProps }: Props = $props();
|
|
15
|
+
let { value = $bindable(), class: className, icon, ...fieldProps }: Props = $props();
|
|
11
16
|
|
|
12
17
|
let show = $state(false);
|
|
13
18
|
let inputType = $derived(show ? 'text' : 'password');
|
|
@@ -16,8 +21,12 @@
|
|
|
16
21
|
<FieldWrapper {...fieldProps}>
|
|
17
22
|
{#snippet formElem(props)}
|
|
18
23
|
<div class="flex w-full items-stretch gap-2">
|
|
19
|
-
<!-- Input itself -->
|
|
20
|
-
<
|
|
24
|
+
<!-- Input itself with optional left icon -->
|
|
25
|
+
<IconInputWrapper {icon} flex>
|
|
26
|
+
{#snippet children({ class: iconClass })}
|
|
27
|
+
<Input type={inputType} bind:value class={cn(iconClass, className)} {...props} />
|
|
28
|
+
{/snippet}
|
|
29
|
+
</IconInputWrapper>
|
|
21
30
|
|
|
22
31
|
<!-- Show/hide button outside input -->
|
|
23
32
|
<button
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
4
5
|
props: FieldWrapperProps<T, U, M> & {
|
|
5
6
|
value?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
icon?: Component;
|
|
6
9
|
};
|
|
7
10
|
exports: {};
|
|
8
11
|
bindings: "value";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
lang="ts"
|
|
3
3
|
generics="I,V, K extends string | number,T extends Record<string, unknown>, U extends FormPath<T>, M"
|
|
4
4
|
>
|
|
5
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
5
6
|
import {
|
|
6
7
|
Select,
|
|
7
8
|
SelectContent,
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
SelectTrigger
|
|
12
13
|
} from '../../../shadcn/components/ui/select/index.js';
|
|
13
14
|
import { cn } from '../../../shadcn/utils.js';
|
|
15
|
+
import type { Component } from 'svelte';
|
|
14
16
|
import type { FormPath } from 'sveltekit-superforms';
|
|
15
17
|
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
16
18
|
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
onchange?: (value: V | undefined) => void;
|
|
24
26
|
class?: string;
|
|
25
27
|
placeholder: string;
|
|
28
|
+
icon?: Component;
|
|
26
29
|
}
|
|
27
30
|
let {
|
|
28
31
|
value = $bindable(undefined),
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
onchange,
|
|
34
37
|
placeholder,
|
|
35
38
|
items,
|
|
39
|
+
icon,
|
|
36
40
|
...fieldProps
|
|
37
41
|
}: Props = $props();
|
|
38
42
|
let valueToItem: Map<V, I> = new Map(items.map((item) => [getValue(item), item] as const));
|
|
@@ -63,11 +67,15 @@
|
|
|
63
67
|
<FieldWrapper {...fieldProps}>
|
|
64
68
|
{#snippet formElem(props)}
|
|
65
69
|
<Select type="single" {...props} bind:value={getKeyFromValue, setValueFromKey}>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
<IconInputWrapper {icon}>
|
|
71
|
+
{#snippet children({ class: iconClass })}
|
|
72
|
+
<SelectTrigger class={cn('w-full cursor-pointer truncate', iconClass, className)}>
|
|
73
|
+
<span class="block truncate">
|
|
74
|
+
{value ? getLabel(valueToItem.get(value)!) : placeholder}
|
|
75
|
+
</span>
|
|
76
|
+
</SelectTrigger>
|
|
77
|
+
{/snippet}
|
|
78
|
+
</IconInputWrapper>
|
|
71
79
|
<SelectContent>
|
|
72
80
|
<SelectGroup>
|
|
73
81
|
<SelectLabel>{fieldProps.label}</SelectLabel>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<I, V, K extends string | number, T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
@@ -10,6 +11,7 @@ declare function $$render<I, V, K extends string | number, T extends Record<stri
|
|
|
10
11
|
onchange?: (value: V | undefined) => void;
|
|
11
12
|
class?: string;
|
|
12
13
|
placeholder: string;
|
|
14
|
+
icon?: Component;
|
|
13
15
|
};
|
|
14
16
|
exports: {};
|
|
15
17
|
bindings: "value";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
lang="ts"
|
|
3
3
|
generics="I,V, K extends string | number,T extends Record<string, unknown>, U extends FormPath<T>, M"
|
|
4
4
|
>
|
|
5
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
5
6
|
import {
|
|
6
7
|
Select,
|
|
7
8
|
SelectContent,
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
SelectTrigger
|
|
12
13
|
} from '../../../shadcn/components/ui/select/index.js';
|
|
13
14
|
import { cn } from '../../../shadcn/utils.js';
|
|
15
|
+
import type { Component } from 'svelte';
|
|
14
16
|
import type { FormPath } from 'sveltekit-superforms';
|
|
15
17
|
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
16
18
|
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
onchange?: (value: V[]) => void;
|
|
24
26
|
class?: string;
|
|
25
27
|
placeholder: string;
|
|
28
|
+
icon?: Component;
|
|
26
29
|
}
|
|
27
30
|
let {
|
|
28
31
|
values = $bindable([]),
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
onchange,
|
|
34
37
|
placeholder,
|
|
35
38
|
items,
|
|
39
|
+
icon,
|
|
36
40
|
...fieldProps
|
|
37
41
|
}: Props = $props();
|
|
38
42
|
let valueToItem: Map<V, I> = new Map(items.map((item) => [getValue(item), item] as const));
|
|
@@ -70,11 +74,15 @@
|
|
|
70
74
|
<FieldWrapper {...fieldProps}>
|
|
71
75
|
{#snippet formElem(props)}
|
|
72
76
|
<Select type="multiple" {...props} bind:value={getKeyFromValue, setValueFromKey}>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
<IconInputWrapper {icon}>
|
|
78
|
+
{#snippet children({ class: iconClass })}
|
|
79
|
+
<SelectTrigger class={cn('w-full cursor-pointer truncate', iconClass, className)}>
|
|
80
|
+
<span class="block truncate">
|
|
81
|
+
{getPreview()}
|
|
82
|
+
</span>
|
|
83
|
+
</SelectTrigger>
|
|
84
|
+
{/snippet}
|
|
85
|
+
</IconInputWrapper>
|
|
78
86
|
<SelectContent>
|
|
79
87
|
<SelectGroup>
|
|
80
88
|
<SelectLabel>{fieldProps.label}</SelectLabel>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<I, V, K extends string | number, T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
@@ -10,6 +11,7 @@ declare function $$render<I, V, K extends string | number, T extends Record<stri
|
|
|
10
11
|
onchange?: (value: V[]) => void;
|
|
11
12
|
class?: string;
|
|
12
13
|
placeholder: string;
|
|
14
|
+
icon?: Component;
|
|
13
15
|
};
|
|
14
16
|
exports: {};
|
|
15
17
|
bindings: "values";
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
3
|
+
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
4
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
5
|
+
import type { Component } from 'svelte';
|
|
2
6
|
import type { FormPath } from 'sveltekit-superforms';
|
|
3
7
|
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
4
|
-
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
5
8
|
|
|
6
9
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
7
10
|
value?: string;
|
|
8
11
|
type?: HTMLInputElement['type'];
|
|
9
12
|
class?: string;
|
|
10
13
|
placeholder?: string;
|
|
14
|
+
icon?: Component;
|
|
11
15
|
}
|
|
12
16
|
let {
|
|
13
17
|
value = $bindable(),
|
|
14
18
|
type = 'text',
|
|
15
19
|
class: className,
|
|
16
20
|
placeholder,
|
|
21
|
+
icon,
|
|
17
22
|
...fieldProps
|
|
18
23
|
}: Props = $props();
|
|
19
24
|
</script>
|
|
20
25
|
|
|
21
26
|
<FieldWrapper {...fieldProps}>
|
|
22
27
|
{#snippet formElem(props)}
|
|
23
|
-
<
|
|
28
|
+
<IconInputWrapper {icon}>
|
|
29
|
+
{#snippet children({ class: iconClass })}
|
|
30
|
+
<Input {type} bind:value class={cn(iconClass, className)} {placeholder} {...props} />
|
|
31
|
+
{/snippet}
|
|
32
|
+
</IconInputWrapper>
|
|
24
33
|
{/snippet}
|
|
25
34
|
</FieldWrapper>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
@@ -6,6 +7,7 @@ declare function $$render<T extends Record<string, unknown>, U extends FormPath<
|
|
|
6
7
|
type?: HTMLInputElement["type"];
|
|
7
8
|
class?: string;
|
|
8
9
|
placeholder?: string;
|
|
10
|
+
icon?: Component;
|
|
9
11
|
};
|
|
10
12
|
exports: {};
|
|
11
13
|
bindings: "value";
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
+
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
3
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
4
|
+
import type { Component } from 'svelte';
|
|
2
5
|
import type { FormPath } from 'sveltekit-superforms';
|
|
3
6
|
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
4
|
-
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
5
7
|
|
|
6
8
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
7
9
|
value?: string | null;
|
|
8
10
|
type?: HTMLInputElement['type'];
|
|
9
11
|
class?: string;
|
|
10
12
|
placeholder?: string;
|
|
13
|
+
icon?: Component;
|
|
11
14
|
}
|
|
12
15
|
let {
|
|
13
16
|
value = $bindable(),
|
|
14
17
|
type = 'text',
|
|
15
18
|
class: className,
|
|
16
19
|
placeholder,
|
|
20
|
+
icon,
|
|
17
21
|
...fieldProps
|
|
18
22
|
}: Props = $props();
|
|
19
23
|
|
|
@@ -28,6 +32,22 @@
|
|
|
28
32
|
|
|
29
33
|
<FieldWrapper {...fieldProps}>
|
|
30
34
|
{#snippet formElem(props)}
|
|
31
|
-
|
|
35
|
+
{#if icon}
|
|
36
|
+
{@const Icon = icon}
|
|
37
|
+
<div class="relative">
|
|
38
|
+
<div class="pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-gray-500">
|
|
39
|
+
<Icon class="size-5" />
|
|
40
|
+
</div>
|
|
41
|
+
<Input
|
|
42
|
+
{type}
|
|
43
|
+
bind:value={get, set}
|
|
44
|
+
class={cn('pl-12', className)}
|
|
45
|
+
{placeholder}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
{:else}
|
|
50
|
+
<Input {type} bind:value={get, set} class={className} {placeholder} {...props} />
|
|
51
|
+
{/if}
|
|
32
52
|
{/snippet}
|
|
33
53
|
</FieldWrapper>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
1
2
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
3
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
4
|
declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
@@ -6,6 +7,7 @@ declare function $$render<T extends Record<string, unknown>, U extends FormPath<
|
|
|
6
7
|
type?: HTMLInputElement["type"];
|
|
7
8
|
class?: string;
|
|
8
9
|
placeholder?: string;
|
|
10
|
+
icon?: Component;
|
|
9
11
|
};
|
|
10
12
|
exports: {};
|
|
11
13
|
bindings: "value";
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
|
|
2
|
-
import
|
|
3
|
-
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
2
|
+
import IconInputWrapper from '../IconInputWrapper.svelte';
|
|
4
3
|
import { Input } from '../../../shadcn/components/ui/input/index.js';
|
|
4
|
+
import { cn } from '../../../shadcn/utils.js';
|
|
5
5
|
import { Time } from '@internationalized/date';
|
|
6
|
+
import type { Component } from 'svelte';
|
|
7
|
+
import type { FormPath } from 'sveltekit-superforms';
|
|
8
|
+
import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
6
9
|
|
|
7
10
|
interface Props extends FieldWrapperProps<T, U, M> {
|
|
8
11
|
value?: Time;
|
|
9
12
|
class?: string;
|
|
10
13
|
step?: HTMLInputElement['step'];
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
icon?: Component;
|
|
11
16
|
}
|
|
12
|
-
let {
|
|
17
|
+
let {
|
|
18
|
+
value = $bindable(),
|
|
19
|
+
class: className,
|
|
20
|
+
step,
|
|
21
|
+
placeholder,
|
|
22
|
+
icon,
|
|
23
|
+
...fieldProps
|
|
24
|
+
}: Props = $props();
|
|
13
25
|
// Getter: format Time as string depending on step
|
|
14
26
|
function get(): string {
|
|
15
27
|
if (!value) return '00:00:00';
|
|
@@ -44,6 +56,17 @@
|
|
|
44
56
|
|
|
45
57
|
<FieldWrapper {...fieldProps}>
|
|
46
58
|
{#snippet formElem(props)}
|
|
47
|
-
<
|
|
59
|
+
<IconInputWrapper {icon}>
|
|
60
|
+
{#snippet children({ class: iconClass })}
|
|
61
|
+
<Input
|
|
62
|
+
type="time"
|
|
63
|
+
bind:value={get, set}
|
|
64
|
+
class={cn(iconClass, className)}
|
|
65
|
+
{placeholder}
|
|
66
|
+
{step}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
{/snippet}
|
|
70
|
+
</IconInputWrapper>
|
|
48
71
|
{/snippet}
|
|
49
72
|
</FieldWrapper>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { Time } from '@internationalized/date';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
1
3
|
import type { FormPath } from 'sveltekit-superforms';
|
|
2
4
|
import { type FieldWrapperProps } from '../FieldWrapper.svelte';
|
|
3
|
-
import { Time } from '@internationalized/date';
|
|
4
5
|
declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
|
|
5
6
|
props: FieldWrapperProps<T, U, M> & {
|
|
6
7
|
value?: Time;
|
|
7
8
|
class?: string;
|
|
8
9
|
step?: HTMLInputElement["step"];
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
icon?: Component;
|
|
9
12
|
};
|
|
10
13
|
exports: {};
|
|
11
14
|
bindings: "value";
|
|
@@ -3,15 +3,16 @@ import ChoiceField from './fields/ChoiceField.svelte';
|
|
|
3
3
|
import ChoiceMultiField from './fields/ChoiceMultiField.svelte';
|
|
4
4
|
import DateField from './fields/DateField.svelte';
|
|
5
5
|
import HexColorField from './fields/HexColorField.svelte';
|
|
6
|
+
import MessageField from './fields/MessageField.svelte';
|
|
6
7
|
import NumberField from './fields/NumberField.svelte';
|
|
7
8
|
import PasswordField from './fields/PasswordField.svelte';
|
|
8
9
|
import SelectField from './fields/SelectField.svelte';
|
|
9
10
|
import SelectMultiField from './fields/SelectMultiField.svelte';
|
|
10
11
|
import TextField from './fields/TextField.svelte';
|
|
11
12
|
import TextFieldNullable from './fields/TextFieldNullable.svelte';
|
|
12
|
-
import MessageField from './fields/MessageField.svelte';
|
|
13
13
|
import TimeField from './fields/TimeField.svelte';
|
|
14
14
|
import WeekdayChoiceField from './fields/WeekdayChoiceField.svelte';
|
|
15
15
|
import WeekdayChoiceMultiField from './fields/WeekdayChoiceMultiField.svelte';
|
|
16
16
|
import Form from './Form.svelte';
|
|
17
|
-
|
|
17
|
+
import IconInputWrapper from './IconInputWrapper.svelte';
|
|
18
|
+
export { Button, ChoiceField, ChoiceMultiField, DateField, Form, HexColorField, IconInputWrapper, MessageField, NumberField, PasswordField, SelectField, SelectMultiField, TextField, TextFieldNullable, TimeField, WeekdayChoiceField, WeekdayChoiceMultiField };
|
|
@@ -3,15 +3,16 @@ import ChoiceField from './fields/ChoiceField.svelte';
|
|
|
3
3
|
import ChoiceMultiField from './fields/ChoiceMultiField.svelte';
|
|
4
4
|
import DateField from './fields/DateField.svelte';
|
|
5
5
|
import HexColorField from './fields/HexColorField.svelte';
|
|
6
|
+
import MessageField from './fields/MessageField.svelte';
|
|
6
7
|
import NumberField from './fields/NumberField.svelte';
|
|
7
8
|
import PasswordField from './fields/PasswordField.svelte';
|
|
8
9
|
import SelectField from './fields/SelectField.svelte';
|
|
9
10
|
import SelectMultiField from './fields/SelectMultiField.svelte';
|
|
10
11
|
import TextField from './fields/TextField.svelte';
|
|
11
12
|
import TextFieldNullable from './fields/TextFieldNullable.svelte';
|
|
12
|
-
import MessageField from './fields/MessageField.svelte';
|
|
13
13
|
import TimeField from './fields/TimeField.svelte';
|
|
14
14
|
import WeekdayChoiceField from './fields/WeekdayChoiceField.svelte';
|
|
15
15
|
import WeekdayChoiceMultiField from './fields/WeekdayChoiceMultiField.svelte';
|
|
16
16
|
import Form from './Form.svelte';
|
|
17
|
-
|
|
17
|
+
import IconInputWrapper from './IconInputWrapper.svelte';
|
|
18
|
+
export { Button, ChoiceField, ChoiceMultiField, DateField, Form, HexColorField, IconInputWrapper, MessageField, NumberField, PasswordField, SelectField, SelectMultiField, TextField, TextFieldNullable, TimeField, WeekdayChoiceField, WeekdayChoiceMultiField };
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { DatabaseError } from 'pg';
|
|
2
1
|
import { fail, message as superFormMessage } from 'sveltekit-superforms';
|
|
3
2
|
/**
|
|
4
3
|
* automatically handle database errors from catch.
|
|
5
4
|
* used in form/action handling in page.server.ts
|
|
6
5
|
*/
|
|
7
6
|
export function handleDbErrorForm(form, message, err) {
|
|
8
|
-
if (err instanceof DatabaseError) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
7
|
+
// if (err instanceof DatabaseError) {
|
|
8
|
+
// console.error(`Database Error ${message}:`, err);
|
|
9
|
+
// return fail(500, { form });
|
|
10
|
+
// }
|
|
12
11
|
console.error(`Unexpected Error ${message}:`, err);
|
|
13
12
|
return fail(500, { form });
|
|
14
13
|
}
|
|
@@ -16,7 +15,9 @@ export function handleDbErrorForm(form, message, err) {
|
|
|
16
15
|
* check if an error returned by a try catch is a duplicate value error in postgre
|
|
17
16
|
*/
|
|
18
17
|
export function isDuplicateDbError(err) {
|
|
19
|
-
|
|
18
|
+
console.error('Checking for duplicate error:', err);
|
|
19
|
+
console.error('Error type:', err instanceof Error ? err.name : typeof err);
|
|
20
|
+
return false;
|
|
20
21
|
}
|
|
21
22
|
export function successMessage(form, options) {
|
|
22
23
|
const message = {
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# Email Utility Package
|
|
2
2
|
|
|
3
|
-
This package provides utilities for sending emails using AWS Simple Email Service (SES).
|
|
3
|
+
This package provides utilities for sending emails using AWS Simple Email Service (SES) API.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **Cloudflare Workers Compatible**: Uses direct AWS SES API calls with fetch instead of AWS SDK
|
|
8
|
+
- **AWS Signature V4**: Implements proper AWS authentication for secure API requests
|
|
9
|
+
- **Zero Heavy Dependencies**: No AWS SDK required, works in any JavaScript runtime with fetch support
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
No additional dependencies required beyond the standard JavaScript runtime. The implementation uses:
|
|
14
|
+
|
|
15
|
+
- Native `fetch` API for HTTP requests
|
|
16
|
+
- Web Crypto API for AWS Signature V4 signing
|
|
17
|
+
- Native `DOMParser` for XML response parsing
|
|
8
18
|
|
|
9
19
|
## Configuration
|
|
10
20
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Signature Version 4 signing utilities for SES API
|
|
3
|
+
* Compatible with Cloudflare Workers and other edge runtimes
|
|
4
|
+
*/
|
|
5
|
+
interface AwsCredentials {
|
|
6
|
+
accessKeyId: string;
|
|
7
|
+
secretAccessKey: string;
|
|
8
|
+
region: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create AWS Signature V4 authorization header
|
|
12
|
+
*/
|
|
13
|
+
export declare function signRequest(method: string, host: string, path: string, body: string, credentials: AwsCredentials, service?: string): Promise<{
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
signedHeaders: string;
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Signature Version 4 signing utilities for SES API
|
|
3
|
+
* Compatible with Cloudflare Workers and other edge runtimes
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a SHA-256 hash
|
|
7
|
+
*/
|
|
8
|
+
async function sha256(message) {
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const data = encoder.encode(message);
|
|
11
|
+
return await crypto.subtle.digest('SHA-256', data);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a SHA-256 HMAC
|
|
15
|
+
*/
|
|
16
|
+
async function hmacSha256(key, message) {
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const keyData = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
|
|
19
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
20
|
+
return await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert ArrayBuffer to hex string
|
|
24
|
+
*/
|
|
25
|
+
function bufferToHex(buffer) {
|
|
26
|
+
return Array.from(new Uint8Array(buffer))
|
|
27
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
28
|
+
.join('');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get signing key for AWS Signature V4
|
|
32
|
+
*/
|
|
33
|
+
async function getSignatureKey(key, dateStamp, regionName, serviceName) {
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const kDate = await hmacSha256(encoder.encode('AWS4' + key), dateStamp);
|
|
36
|
+
const kRegion = await hmacSha256(kDate, regionName);
|
|
37
|
+
const kService = await hmacSha256(kRegion, serviceName);
|
|
38
|
+
const kSigning = await hmacSha256(kService, 'aws4_request');
|
|
39
|
+
return kSigning;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create AWS Signature V4 authorization header
|
|
43
|
+
*/
|
|
44
|
+
export async function signRequest(method, host, path, body, credentials, service = 'ses') {
|
|
45
|
+
const { accessKeyId, secretAccessKey, region } = credentials;
|
|
46
|
+
// Create timestamp
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const amzDate = now
|
|
49
|
+
.toISOString()
|
|
50
|
+
.replace(/[:-]|\.\d{3}/g, '')
|
|
51
|
+
.slice(0, -1); // Format: YYYYMMDD'T'HHMMSS'Z'
|
|
52
|
+
const dateStamp = amzDate.slice(0, 8); // Format: YYYYMMDD
|
|
53
|
+
// Hash the request body
|
|
54
|
+
const payloadHash = bufferToHex(await sha256(body));
|
|
55
|
+
// Create canonical headers
|
|
56
|
+
const canonicalHeaders = `host:${host}\n` + `x-amz-date:${amzDate}\n`;
|
|
57
|
+
const signedHeaders = 'host;x-amz-date';
|
|
58
|
+
// Create canonical request
|
|
59
|
+
const canonicalRequest = `${method}\n` +
|
|
60
|
+
`${path}\n` +
|
|
61
|
+
`\n` + // Empty query string
|
|
62
|
+
`${canonicalHeaders}\n` +
|
|
63
|
+
`${signedHeaders}\n` +
|
|
64
|
+
`${payloadHash}`;
|
|
65
|
+
// Create string to sign
|
|
66
|
+
const algorithm = 'AWS4-HMAC-SHA256';
|
|
67
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
68
|
+
const canonicalRequestHash = bufferToHex(await sha256(canonicalRequest));
|
|
69
|
+
const stringToSign = `${algorithm}\n` + `${amzDate}\n` + `${credentialScope}\n` + `${canonicalRequestHash}`;
|
|
70
|
+
// Calculate signature
|
|
71
|
+
const signingKey = await getSignatureKey(secretAccessKey, dateStamp, region, service);
|
|
72
|
+
const signature = bufferToHex(await hmacSha256(signingKey, stringToSign));
|
|
73
|
+
// Create authorization header
|
|
74
|
+
const authorizationHeader = `${algorithm} ` +
|
|
75
|
+
`Credential=${accessKeyId}/${credentialScope}, ` +
|
|
76
|
+
`SignedHeaders=${signedHeaders}, ` +
|
|
77
|
+
`Signature=${signature}`;
|
|
78
|
+
return {
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: authorizationHeader,
|
|
81
|
+
'X-Amz-Date': amzDate,
|
|
82
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
83
|
+
},
|
|
84
|
+
signedHeaders
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -10,7 +10,8 @@ export interface SendEmailOptions {
|
|
|
10
10
|
replyTo?: string | string[];
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Send an email using AWS SES.
|
|
13
|
+
* Send an email using AWS SES API.
|
|
14
|
+
* Uses AWS Signature V4 signing and fetch API for Cloudflare Workers compatibility.
|
|
14
15
|
*
|
|
15
16
|
* Environment variables required:
|
|
16
17
|
* - AWS_REGION
|
package/dist/utils/email/ses.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// Create SES client once at module level for reuse across function calls
|
|
3
|
-
const sesClient = new SESClient({ region: process.env.AWS_REGION });
|
|
1
|
+
import { signRequest } from './aws-signer.js';
|
|
4
2
|
/**
|
|
5
|
-
* Send an email using AWS SES.
|
|
3
|
+
* Send an email using AWS SES API.
|
|
4
|
+
* Uses AWS Signature V4 signing and fetch API for Cloudflare Workers compatibility.
|
|
6
5
|
*
|
|
7
6
|
* Environment variables required:
|
|
8
7
|
* - AWS_REGION
|
|
@@ -39,48 +38,105 @@ export async function sendEmail(options) {
|
|
|
39
38
|
if (!toAddresses || toAddresses.length === 0) {
|
|
40
39
|
throw new Error('sendEmail: at least one valid recipient is required (to)');
|
|
41
40
|
}
|
|
41
|
+
// Get AWS credentials from environment
|
|
42
|
+
const region = process.env.AWS_REGION;
|
|
43
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
44
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
45
|
+
if (!region || !accessKeyId || !secretAccessKey) {
|
|
46
|
+
throw new Error('sendEmail: missing required environment variables (AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)');
|
|
47
|
+
}
|
|
42
48
|
// Format source with optional fromName
|
|
43
49
|
const source = fromName ? `${fromName} <${from}>` : from;
|
|
44
|
-
// Build
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Build form-encoded request body for SES API
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
params.append('Action', 'SendEmail');
|
|
53
|
+
params.append('Source', source);
|
|
54
|
+
// Add destination addresses
|
|
55
|
+
toAddresses.forEach((addr, i) => params.append(`Destination.ToAddresses.member.${i + 1}`, addr));
|
|
56
|
+
if (ccAddresses) {
|
|
57
|
+
ccAddresses.forEach((addr, i) => params.append(`Destination.CcAddresses.member.${i + 1}`, addr));
|
|
58
|
+
}
|
|
59
|
+
if (bccAddresses) {
|
|
60
|
+
bccAddresses.forEach((addr, i) => params.append(`Destination.BccAddresses.member.${i + 1}`, addr));
|
|
61
|
+
}
|
|
62
|
+
// Add reply-to addresses
|
|
63
|
+
if (replyToAddresses) {
|
|
64
|
+
replyToAddresses.forEach((addr, i) => params.append(`ReplyToAddresses.member.${i + 1}`, addr));
|
|
65
|
+
}
|
|
66
|
+
// Add message subject
|
|
67
|
+
params.append('Message.Subject.Data', subject);
|
|
68
|
+
params.append('Message.Subject.Charset', 'UTF-8');
|
|
69
|
+
// Add message body
|
|
52
70
|
if (html) {
|
|
53
|
-
Message.Body.Html
|
|
54
|
-
|
|
55
|
-
Data: html
|
|
56
|
-
};
|
|
71
|
+
params.append('Message.Body.Html.Data', html);
|
|
72
|
+
params.append('Message.Body.Html.Charset', 'UTF-8');
|
|
57
73
|
}
|
|
58
74
|
if (text) {
|
|
59
|
-
Message.Body.Text
|
|
60
|
-
|
|
61
|
-
Data: text
|
|
62
|
-
};
|
|
75
|
+
params.append('Message.Body.Text.Data', text);
|
|
76
|
+
params.append('Message.Body.Text.Charset', 'UTF-8');
|
|
63
77
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ToAddresses: toAddresses,
|
|
68
|
-
CcAddresses: ccAddresses,
|
|
69
|
-
BccAddresses: bccAddresses
|
|
70
|
-
},
|
|
71
|
-
Message,
|
|
72
|
-
ReplyToAddresses: replyToAddresses
|
|
73
|
-
};
|
|
78
|
+
const body = params.toString();
|
|
79
|
+
const host = `email.${region}.amazonaws.com`;
|
|
80
|
+
const path = '/';
|
|
74
81
|
try {
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
// Sign the request
|
|
83
|
+
const { headers } = await signRequest('POST', host, path, body, {
|
|
84
|
+
accessKeyId,
|
|
85
|
+
secretAccessKey,
|
|
86
|
+
region
|
|
87
|
+
});
|
|
88
|
+
// Make the API request
|
|
89
|
+
const response = await fetch(`https://${host}${path}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
...headers,
|
|
93
|
+
Host: host,
|
|
94
|
+
'Content-Length': body.length.toString()
|
|
95
|
+
},
|
|
96
|
+
body
|
|
97
|
+
});
|
|
98
|
+
const responseText = await response.text();
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
// Parse error response
|
|
101
|
+
let errorMessage = 'Unknown error';
|
|
102
|
+
let errorCode;
|
|
103
|
+
try {
|
|
104
|
+
const parser = new DOMParser();
|
|
105
|
+
const xmlDoc = parser.parseFromString(responseText, 'text/xml');
|
|
106
|
+
const errorNode = xmlDoc.querySelector('Error');
|
|
107
|
+
if (errorNode) {
|
|
108
|
+
errorCode = errorNode.querySelector('Code')?.textContent || undefined;
|
|
109
|
+
errorMessage = errorNode.querySelector('Message')?.textContent || errorMessage;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
errorMessage = responseText || `HTTP ${response.status} ${response.statusText}`;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`sendEmail: failed to send email: ${JSON.stringify({ message: errorMessage, code: errorCode })}`);
|
|
116
|
+
}
|
|
117
|
+
// Parse success response for MessageId
|
|
118
|
+
try {
|
|
119
|
+
const parser = new DOMParser();
|
|
120
|
+
const xmlDoc = parser.parseFromString(responseText, 'text/xml');
|
|
121
|
+
const messageId = xmlDoc.querySelector('MessageId')?.textContent;
|
|
122
|
+
if (!messageId) {
|
|
123
|
+
throw new Error('sendEmail: SES response did not contain a MessageId');
|
|
124
|
+
}
|
|
125
|
+
return messageId;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof Error && err.message.includes('MessageId')) {
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`sendEmail: failed to parse SES response: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
132
|
}
|
|
80
|
-
return res.MessageId;
|
|
81
133
|
}
|
|
82
134
|
catch (err) {
|
|
83
|
-
//
|
|
135
|
+
// Re-throw if already formatted
|
|
136
|
+
if (err instanceof Error && err.message.startsWith('sendEmail:')) {
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
// Normalize other errors
|
|
84
140
|
const message = err instanceof Error ? err.message : String(err);
|
|
85
141
|
let code;
|
|
86
142
|
if (err && typeof err === 'object') {
|
|
@@ -89,7 +145,6 @@ export async function sendEmail(options) {
|
|
|
89
145
|
code = e['name'];
|
|
90
146
|
}
|
|
91
147
|
const details = { message, code };
|
|
92
|
-
// Re-throw a clear error for the caller to handle
|
|
93
148
|
throw new Error(`sendEmail: failed to send email: ${JSON.stringify(details)}`);
|
|
94
149
|
}
|
|
95
150
|
}
|
package/package.json
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.7.
|
|
6
|
+
"version": "0.7.3",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Webamoki/Web-svelte"
|
|
11
|
+
},
|
|
8
12
|
"files": [
|
|
9
13
|
"dist",
|
|
10
14
|
"!dist/**/*.test.*",
|
|
@@ -71,6 +75,7 @@
|
|
|
71
75
|
"@changesets/cli": "^2.29.7",
|
|
72
76
|
"@eslint/compat": "^1.4.1",
|
|
73
77
|
"@eslint/js": "^9.39.1",
|
|
78
|
+
"@sveltejs/adapter-cloudflare": "^7.2.8",
|
|
74
79
|
"@sveltejs/kit": "^2.48.4",
|
|
75
80
|
"@sveltejs/package": "^2.5.4",
|
|
76
81
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
@@ -78,7 +83,6 @@
|
|
|
78
83
|
"@tailwindcss/typography": "^0.5.19",
|
|
79
84
|
"@tailwindcss/vite": "^4.1.17",
|
|
80
85
|
"@types/node": "^22.19.1",
|
|
81
|
-
"@types/pg": "^8.15.6",
|
|
82
86
|
"@types/ramda": "^0.31.1",
|
|
83
87
|
"@types/sorted-array-functions": "^1.3.3",
|
|
84
88
|
"clsx": "^2.1.1",
|
|
@@ -106,7 +110,6 @@
|
|
|
106
110
|
"svelte"
|
|
107
111
|
],
|
|
108
112
|
"dependencies": {
|
|
109
|
-
"@aws-sdk/client-ses": "^3.948.0",
|
|
110
113
|
"@internationalized/date": "^3.10.0",
|
|
111
114
|
"@lucide/svelte": "^0.553.0",
|
|
112
115
|
"@sveltejs/adapter-auto": "^7.0.0",
|
|
@@ -115,7 +118,6 @@
|
|
|
115
118
|
"devalue": "^5.5.0",
|
|
116
119
|
"drizzle-orm": "^0.44.7",
|
|
117
120
|
"formsnap": "^2.0.1",
|
|
118
|
-
"pg": "^8.16.3",
|
|
119
121
|
"ramda": "^0.31.3",
|
|
120
122
|
"sorted-array-functions": "^1.3.0",
|
|
121
123
|
"svelte-awesome-color-picker": "^4.1.0",
|