Residents frontend page (#82)
* create modal component * refactor app layout and add residents page * add residents page * fix story * fix error * add resident edit * allow resident delete * remove page transitions, use default actions
This commit is contained in:
parent
0f95f330ef
commit
40bf33a9e5
19 changed files with 415 additions and 39 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"nav_greeting": "Hello {name}!",
|
"nav_greeting": "Hello {name}!",
|
||||||
"nav_menu_sms": "SMS",
|
"nav_menu_sms": "SMS",
|
||||||
|
"nav_menu_residents": "Residents",
|
||||||
"nav_menu_settings": "Settings",
|
"nav_menu_settings": "Settings",
|
||||||
"nav_menu_logout": "Logout",
|
"nav_menu_logout": "Logout",
|
||||||
"login_tab_login": "Login",
|
"login_tab_login": "Login",
|
||||||
|
|
@ -21,6 +22,15 @@
|
||||||
"sms_label_phone": "Phone Number",
|
"sms_label_phone": "Phone Number",
|
||||||
"sms_label_message": "Message",
|
"sms_label_message": "Message",
|
||||||
"sms_button_submit": "Send Message",
|
"sms_button_submit": "Send Message",
|
||||||
|
"residents_title": "Residents",
|
||||||
|
"residents_button_new": "New Resident",
|
||||||
|
"residents_table_edit": "Edit",
|
||||||
|
"residents_modal_title_new": "Create a Resident",
|
||||||
|
"residents_modal_title_edit": "Edit Resident",
|
||||||
|
"residents_modal_submit": "Submit",
|
||||||
|
"residents_modal_delete": "Delete",
|
||||||
|
"residents_modal_label_name": "Name",
|
||||||
|
"residents_modal_label_phone": "Phone Number",
|
||||||
"settings_title": "Settings",
|
"settings_title": "Settings",
|
||||||
"settings_category_twilio": "Twilio Config",
|
"settings_category_twilio": "Twilio Config",
|
||||||
"settings_twilio_account_sid": "Account SID",
|
"settings_twilio_account_sid": "Account SID",
|
||||||
|
|
|
||||||
36
src/lib/components/Actions/Modal/Modal.stories.svelte
Normal file
36
src/lib/components/Actions/Modal/Modal.stories.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import Modal from './Modal.svelte';
|
||||||
|
import ModalBody from './ModalBody.svelte';
|
||||||
|
import ModalActions from './ModalActions.svelte';
|
||||||
|
import Button from '../Button.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Actions/Modal',
|
||||||
|
component: Modal,
|
||||||
|
argTypes: {
|
||||||
|
open: {
|
||||||
|
control: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let dialog: HTMLDialogElement | undefined = $state(undefined);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet template({ children: _, ...props }: Partial<ComponentProps<typeof Modal>>)}
|
||||||
|
<Button onclick={() => dialog?.showModal()}>Open</Button>
|
||||||
|
<Modal {...props} backdrop bind:dialog>
|
||||||
|
<ModalBody>
|
||||||
|
<h3 class="text-lg font-bold">Hello!</h3>
|
||||||
|
<p class="py-4">Press ESC key or click the button below to close</p>
|
||||||
|
<ModalActions>
|
||||||
|
<Button onclick={() => dialog?.close()}>Close</Button>
|
||||||
|
</ModalActions>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Story name="Default" children={template} />
|
||||||
21
src/lib/components/Actions/Modal/Modal.svelte
Normal file
21
src/lib/components/Actions/Modal/Modal.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = SvelteHTMLElements['dialog'] & {
|
||||||
|
backdrop?: boolean;
|
||||||
|
dialog?: HTMLDialogElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { backdrop, dialog = $bindable(), children, class: className, ...props }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog {...props} class={twMerge('modal', clsx(className))} bind:this={dialog}>
|
||||||
|
{@render children?.()}
|
||||||
|
{#if backdrop}
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</dialog>
|
||||||
13
src/lib/components/Actions/Modal/ModalActions.svelte
Normal file
13
src/lib/components/Actions/Modal/ModalActions.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = SvelteHTMLElements['div'];
|
||||||
|
|
||||||
|
let { children, class: className, ...props }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...props} class={twMerge('modal-action', clsx(className))}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
13
src/lib/components/Actions/Modal/ModalBody.svelte
Normal file
13
src/lib/components/Actions/Modal/ModalBody.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = SvelteHTMLElements['div'];
|
||||||
|
|
||||||
|
let { children, class: className, ...props }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...props} class={twMerge('modal-box', clsx(className))}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
3
src/lib/components/Actions/Modal/index.ts
Normal file
3
src/lib/components/Actions/Modal/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as Modal } from './Modal.svelte';
|
||||||
|
export { default as ModalActions } from './ModalActions.svelte';
|
||||||
|
export { default as ModalBody } from './ModalBody.svelte';
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { default as Button } from './Button.svelte';
|
export { default as Button } from './Button.svelte';
|
||||||
|
export * from './Modal/';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
let { start, center, end }: { start?: Snippet; center?: Snippet; end?: Snippet } = $props();
|
let { start, center, end }: { start?: Snippet; center?: Snippet; end?: Snippet } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="navbar justify-between bg-base-200 px-4">
|
<header class="navbar justify-between rounded-box bg-base-200 px-4">
|
||||||
<div class="navbar-start">{@render start?.()}</div>
|
<div class="navbar-start">{@render start?.()}</div>
|
||||||
<div class="navbar-center">{@render center?.()}</div>
|
<div class="navbar-center">{@render center?.()}</div>
|
||||||
<div class="navbar-end">{@render end?.()}</div>
|
<div class="navbar-end">{@render end?.()}</div>
|
||||||
|
|
|
||||||
28
src/lib/components/Residents/ResidentTable.stories.svelte
Normal file
28
src/lib/components/Residents/ResidentTable.stories.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import ResidentTable from './ResidentTable.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Residents/Resident Table',
|
||||||
|
component: ResidentTable,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet template(props: ComponentProps<typeof ResidentTable>)}
|
||||||
|
<ResidentTable {...props} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
children={template}
|
||||||
|
args={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '99fc03e9-3ed1-4047-b9ad-0bf2a9025eaa',
|
||||||
|
name: 'Resident 1',
|
||||||
|
phoneNumber: '111-1111',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
53
src/lib/components/Residents/ResidentTable.svelte
Normal file
53
src/lib/components/Residents/ResidentTable.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
export type ResidentItem = Pick<Resident, 'id' | 'name' | 'phoneNumber'>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Resident } from '@prisma/client';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { UserRoundPen } from 'lucide-svelte';
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from '../Actions/Button.svelte';
|
||||||
|
import { messages } from '$lib/i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items?: ResidentItem[];
|
||||||
|
onEdit?: (resident: ResidentItem) => void;
|
||||||
|
} & Omit<SvelteHTMLElements['table'], 'children'>;
|
||||||
|
|
||||||
|
let { items, onEdit, class: className, ...props }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<table {...props} class={twMerge('table', clsx(className))}>
|
||||||
|
<!-- head -->
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-base-100">
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Phone Number</th>
|
||||||
|
{#if onEdit}
|
||||||
|
<th class="flex justify-end"><UserRoundPen class="mx-3" /></th>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- items -->
|
||||||
|
{#if items}
|
||||||
|
{#each items as resident, index (resident.id)}
|
||||||
|
<tr class="hover">
|
||||||
|
<th>{index + 1}</th>
|
||||||
|
<td>{resident.name}</td>
|
||||||
|
<td>{resident.phoneNumber}</td>
|
||||||
|
{#if onEdit}
|
||||||
|
<td class="text-end">
|
||||||
|
<Button size="sm" color="accent" onclick={() => onEdit(resident)}>
|
||||||
|
{messages.residents_table_edit()}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
2
src/lib/components/Residents/index.ts
Normal file
2
src/lib/components/Residents/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as ResidentTable } from './ResidentTable.svelte';
|
||||||
|
export * from './ResidentTable.svelte';
|
||||||
|
|
@ -8,16 +8,16 @@
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="layout">
|
<ParaglideJS {i18n}>
|
||||||
<ParaglideJS {i18n}>
|
<ClerkProvider publishableKey={PUBLIC_CLERK_PUBLISHABLE_KEY}>
|
||||||
<ClerkProvider publishableKey={PUBLIC_CLERK_PUBLISHABLE_KEY}>
|
<div class="layout">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</ClerkProvider>
|
</div>
|
||||||
</ParaglideJS>
|
</ClerkProvider>
|
||||||
</div>
|
</ParaglideJS>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.layout {
|
.layout {
|
||||||
@apply h-screen w-screen bg-base-100;
|
@apply h-screen w-screen p-8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
import { messages } from '$lib/i18n';
|
import { messages } from '$lib/i18n';
|
||||||
import 'clerk-sveltekit/client';
|
import 'clerk-sveltekit/client';
|
||||||
import SignOutButton from 'clerk-sveltekit/client/SignOutButton.svelte';
|
import SignOutButton from 'clerk-sveltekit/client/SignOutButton.svelte';
|
||||||
import { Cog, LogOut, MessageCircleMore } from 'lucide-svelte';
|
import { Cog, LogOut, MessageCircleMore, UsersRound } from 'lucide-svelte';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
data: PageData;
|
data: LayoutData;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { children, data }: Props = $props();
|
let { children, data }: Props = $props();
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="menu dropdown-content menu-lg z-[1] mt-4 w-52 rounded-box bg-base-200 p-2 text-right shadow"
|
class="menu dropdown-content menu-lg z-[1] mt-4 w-52 rounded-box border border-neutral bg-base-200 p-2 text-right shadow"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<button onclick={() => goto('/app/sms')}>
|
<button onclick={() => goto('/app/sms')}>
|
||||||
|
|
@ -73,6 +73,12 @@
|
||||||
{messages.nav_menu_sms()}
|
{messages.nav_menu_sms()}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onclick={() => goto('/app/residents')}>
|
||||||
|
<UsersRound />
|
||||||
|
{messages.nav_menu_residents()}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button onclick={() => goto('/app/settings')}>
|
<button onclick={() => goto('/app/settings')}>
|
||||||
<Cog />
|
<Cog />
|
||||||
|
|
@ -89,20 +95,24 @@
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<Navbar>
|
<div class="flex h-full flex-col items-stretch gap-4">
|
||||||
{#snippet start()}
|
<Navbar>
|
||||||
<Button onclick={() => goto('/app')}>
|
{#snippet start()}
|
||||||
<h2 class="prose prose-xl">Hestia</h2>
|
<Button onclick={() => goto('/app')} class="rounded-box" color="ghost">
|
||||||
</Button>
|
<h2 class="prose prose-xl">Hestia</h2>
|
||||||
{/snippet}
|
</Button>
|
||||||
{#snippet center()}
|
{/snippet}
|
||||||
<h1 class="prose prose-2xl">Svelte</h1>
|
{#snippet center()}
|
||||||
{/snippet}
|
<h1 class="prose prose-2xl">Svelte</h1>
|
||||||
{#snippet end()}
|
{/snippet}
|
||||||
<div class="flex items-center gap-3">
|
{#snippet end()}
|
||||||
<p class="prose prose-lg">{message}</p>
|
<div class="flex items-center gap-3">
|
||||||
{@render userMenu()}
|
<p class="prose prose-lg">{message}</p>
|
||||||
</div>
|
{@render userMenu()}
|
||||||
{/snippet}
|
</div>
|
||||||
</Navbar>
|
{/snippet}
|
||||||
{@render children()}
|
</Navbar>
|
||||||
|
<div class="h-full rounded-box bg-base-200 p-8">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
90
src/routes/app/residents/+page.server.ts
Normal file
90
src/routes/app/residents/+page.server.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { PhoneRegex } from '$lib/regex/phone.js';
|
||||||
|
import { logger } from '$lib/server/logger/index.js';
|
||||||
|
import { prisma } from '$lib/server/prisma';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import zod from 'zod';
|
||||||
|
|
||||||
|
export const load = async ({ locals }) => {
|
||||||
|
const residents = await prisma.resident.findMany({
|
||||||
|
where: {
|
||||||
|
tenantId: locals.tenant.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
phoneNumber: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
residents: residents,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
upsert: async (event) => {
|
||||||
|
const form = await event.request.formData();
|
||||||
|
|
||||||
|
if (!form.has('name')) {
|
||||||
|
return fail(400, { error: 'phone_missing' });
|
||||||
|
}
|
||||||
|
if (!form.get('phoneNumber')) {
|
||||||
|
return fail(400, { error: 'message_missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = form.get('id');
|
||||||
|
if (id && typeof id !== 'string') {
|
||||||
|
return fail(400, { error: 'invalid_id' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = form.get('name');
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
return fail(400, { error: 'invalid_name' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
success: phoneSuccess,
|
||||||
|
data: phone,
|
||||||
|
error: phoneError,
|
||||||
|
} = zod.string().regex(PhoneRegex).safeParse(form.get('phoneNumber'));
|
||||||
|
if (!phoneSuccess) {
|
||||||
|
logger.error(phoneError);
|
||||||
|
return fail(400, { error: 'invalid_phone' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.resident.upsert({
|
||||||
|
where: {
|
||||||
|
id: id ?? '',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: name,
|
||||||
|
phoneNumber: phone,
|
||||||
|
tenantId: event.locals.tenant.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: name,
|
||||||
|
phoneNumber: phone,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delete: async (event) => {
|
||||||
|
const form = await event.request.formData();
|
||||||
|
|
||||||
|
logger.info('Deleting Resident');
|
||||||
|
|
||||||
|
if (!form.has('id')) {
|
||||||
|
return fail(400, { error: 'id_missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = form.get('id');
|
||||||
|
if (typeof id !== 'string') {
|
||||||
|
return fail(400, { error: 'invalid_id' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.resident.delete({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
98
src/routes/app/residents/+page.svelte
Normal file
98
src/routes/app/residents/+page.svelte
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import { Button, Modal, ModalActions, ModalBody } from '$lib/components/Actions';
|
||||||
|
import { TextInput } from '$lib/components/DataInput';
|
||||||
|
import { ResidentTable, type ResidentItem } from '$lib/components/Residents';
|
||||||
|
import { messages } from '$lib/i18n';
|
||||||
|
import { Phone, UserRound, UserRoundPlus, X } from 'lucide-svelte';
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: PageData;
|
||||||
|
form: ActionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
let residents = $derived(data.residents);
|
||||||
|
|
||||||
|
let dialog: HTMLDialogElement | undefined = $state(undefined);
|
||||||
|
let form: HTMLFormElement | undefined = $state(undefined);
|
||||||
|
|
||||||
|
let resident: ResidentItem | undefined = $state(undefined);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
backdrop
|
||||||
|
bind:dialog
|
||||||
|
onclose={() => {
|
||||||
|
resident = undefined;
|
||||||
|
form?.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl">
|
||||||
|
{resident
|
||||||
|
? messages.residents_modal_title_edit()
|
||||||
|
: messages.residents_modal_title_new()}
|
||||||
|
</h2>
|
||||||
|
<form method="dialog">
|
||||||
|
<Button color="ghost" shape="square" size="sm">
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="?/upsert" bind:this={form} use:enhance>
|
||||||
|
{#if resident}
|
||||||
|
<input type="hidden" name="id" value={resident.id} />
|
||||||
|
{/if}
|
||||||
|
<TextInput bordered name="name" value={resident?.name}>
|
||||||
|
{#snippet label()}
|
||||||
|
<UserRound size="18" />
|
||||||
|
{messages.residents_modal_label_name()}
|
||||||
|
{/snippet}
|
||||||
|
</TextInput>
|
||||||
|
<TextInput bordered name="phoneNumber" type={'tel'} value={resident?.phoneNumber}>
|
||||||
|
{#snippet label()}
|
||||||
|
<Phone size="18" />
|
||||||
|
{messages.residents_modal_label_phone()}
|
||||||
|
{/snippet}
|
||||||
|
</TextInput>
|
||||||
|
<ModalActions class="flex">
|
||||||
|
<Button
|
||||||
|
class="grow"
|
||||||
|
type="submit"
|
||||||
|
formaction="?/delete"
|
||||||
|
color="error"
|
||||||
|
onclick={() => dialog?.close()}
|
||||||
|
>
|
||||||
|
{messages.residents_modal_delete()}
|
||||||
|
</Button>
|
||||||
|
<Button class="grow" type="submit" color="primary" onclick={() => dialog?.close()}>
|
||||||
|
{messages.residents_modal_submit()}
|
||||||
|
</Button>
|
||||||
|
</ModalActions>
|
||||||
|
</form>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div class="mx-auto flex max-w-5xl flex-col items-stretch gap-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl">{messages.residents_title()}</h1>
|
||||||
|
<Button
|
||||||
|
onclick={() => {
|
||||||
|
dialog?.showModal();
|
||||||
|
}}
|
||||||
|
color="primary"><UserRoundPlus size="18" />{messages.residents_button_new()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<ResidentTable
|
||||||
|
items={residents}
|
||||||
|
onEdit={(r) => {
|
||||||
|
resident = r;
|
||||||
|
dialog?.showModal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
@ -27,7 +27,7 @@ export const load = async (event) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
update: async (event) => {
|
default: async (event) => {
|
||||||
const form = await event.request.formData();
|
const form = await event.request.formData();
|
||||||
const tenantId = event.locals.tenant.id;
|
const tenantId = event.locals.tenant.id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { Button } from '$lib/components/Actions';
|
import { Button } from '$lib/components/Actions';
|
||||||
import { TextInput } from '$lib/components/DataInput';
|
import { TextInput } from '$lib/components/DataInput';
|
||||||
|
import Divider from '$lib/components/Layout/Divider.svelte';
|
||||||
import { messages } from '$lib/i18n';
|
import { messages } from '$lib/i18n';
|
||||||
import { Fingerprint, KeyRound, PhoneOutgoing } from 'lucide-svelte';
|
import { Fingerprint, KeyRound, PhoneOutgoing } from 'lucide-svelte';
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
import Divider from '$lib/components/Layout/Divider.svelte';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
|
|
@ -17,18 +16,18 @@
|
||||||
let configs = $derived(form?.configs ?? data.configs);
|
let configs = $derived(form?.configs ?? data.configs);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page" transition:fade>
|
<div class="page">
|
||||||
<div class="card w-full max-w-xl bg-base-200 px-4 pt-4 shadow-xl">
|
<div class="card w-full max-w-xl bg-base-200 px-4 pt-4 shadow-xl">
|
||||||
<div class="card-title justify-center">
|
<div class="card-title justify-center">
|
||||||
<h2 class="text-2xl font-semibold">{messages.settings_title()}</h2>
|
<h2 class="text-2xl font-semibold">{messages.settings_title()}</h2>
|
||||||
{#if form?.error}
|
{#if form?.error}
|
||||||
<div role="alert" class="alert alert-error absolute -top-20" transition:fade>
|
<div role="alert" class="alert alert-error absolute -top-20">
|
||||||
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
|
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
|
||||||
<span>{form.error}</span>
|
<span>{form.error}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<form id="sms" method="POST" action="?/update" use:enhance>
|
<form id="sms" method="POST" use:enhance>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<Divider />
|
<Divider />
|
||||||
<!-- Twilio -->
|
<!-- Twilio -->
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export const load = async (event) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
push: async (event) => {
|
default: async (event) => {
|
||||||
const form = await event.request.formData();
|
const form = await event.request.formData();
|
||||||
|
|
||||||
if (!form.has('phone')) {
|
if (!form.has('phone')) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
import { Link } from '$lib/components/Navigation';
|
import { Link } from '$lib/components/Navigation';
|
||||||
import { messages } from '$lib/i18n';
|
import { messages } from '$lib/i18n';
|
||||||
import { CircleX, MessageCircleMore, PhoneOutgoing, TriangleAlert } from 'lucide-svelte';
|
import { CircleX, MessageCircleMore, PhoneOutgoing, TriangleAlert } from 'lucide-svelte';
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -25,7 +24,7 @@
|
||||||
<MessageCircleMore size="18" /> {messages.sms_label_message()}
|
<MessageCircleMore size="18" /> {messages.sms_label_message()}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="page" transition:fade>
|
<div class="page">
|
||||||
<div class="card bg-base-200 px-4 pt-4 shadow-xl">
|
<div class="card bg-base-200 px-4 pt-4 shadow-xl">
|
||||||
{#if form?.error}
|
{#if form?.error}
|
||||||
<Alert status="error">
|
<Alert status="error">
|
||||||
|
|
@ -49,7 +48,7 @@
|
||||||
<div class="card-title justify-center">
|
<div class="card-title justify-center">
|
||||||
<h2 class="text-2xl font-semibold">{messages.sms_prompt()}</h2>
|
<h2 class="text-2xl font-semibold">{messages.sms_prompt()}</h2>
|
||||||
</div>
|
</div>
|
||||||
<form id="sms" method="POST" action="?/push" use:enhance>
|
<form id="sms" method="POST" use:enhance>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled={!data.isTwilioConfigured}
|
disabled={!data.isTwilioConfigured}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue