Residents frontend page #82
6 changed files with 185 additions and 0 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,12 @@
|
||||||
"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_modal_title": "Create a Resident",
|
||||||
|
"residents_modal_submit": "Submit",
|
||||||
|
"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",
|
||||||
|
|
|
||||||
21
src/lib/components/Residents/ResidentTable.stories.svelte
Normal file
21
src/lib/components/Residents/ResidentTable.stories.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<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,
|
||||||
|
argTypes: {},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet template(props: ComponentProps<typeof ResidentTable>)}
|
||||||
|
<ResidentTable {...props} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
children={template}
|
||||||
|
args={{ items: [{ name: 'Resident 1', phoneNumber: '111-1111' }] }}
|
||||||
|
/>
|
||||||
33
src/lib/components/Residents/ResidentTable.svelte
Normal file
33
src/lib/components/Residents/ResidentTable.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Resident } from '@prisma/client';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: Pick<Resident, 'id' | 'name' | 'phoneNumber'>[];
|
||||||
|
} & Omit<SvelteHTMLElements['table'], 'children'>;
|
||||||
|
|
||||||
|
let { items, 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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- items -->
|
||||||
|
{#each items as resident, index (resident.id)}
|
||||||
|
<tr class="hover">
|
||||||
|
<th>{index + 1}</th>
|
||||||
|
<td>{resident.name}</td>
|
||||||
|
<td>{resident.phoneNumber}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
1
src/lib/components/Residents/index.ts
Normal file
1
src/lib/components/Residents/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ResidentTable } from './ResidentTable.svelte';
|
||||||
58
src/routes/app/residents/+page.server.ts
Normal file
58
src/routes/app/residents/+page.server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
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 = {
|
||||||
|
default: async (event) => {
|
||||||
|
const form = await event.request.formData();
|
||||||
|
|
||||||
|
if (!form.has('name')) {
|
||||||
|
return fail(400, { error: 'phone_missing' });
|
||||||
|
I'd like to work out a system with you guys for handling these I'd like to work out a system with you guys for handling these
|
|||||||
|
}
|
||||||
|
if (!form.get('phoneNumber')) {
|
||||||
|
return fail(400, { error: 'message_missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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.create({
|
||||||
|
data: {
|
||||||
|
name: name,
|
||||||
|
phoneNumber: phone,
|
||||||
|
tenantId: event.locals.tenant.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
65
src/routes/app/residents/+page.svelte
Normal file
65
src/routes/app/residents/+page.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<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 } from '$lib/components/Residents';
|
||||||
|
import { messages } from '$lib/i18n';
|
||||||
|
import { Phone, UserRound, UserRoundPlus } from 'lucide-svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: PageData;
|
||||||
|
form: ActionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
let residents = $derived(data.residents);
|
||||||
|
|
||||||
|
let dialog = $state<HTMLDialogElement | undefined>(undefined);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal backdrop bind:dialog>
|
||||||
|
<ModalBody>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl">{messages.residents_modal_title()}</h2>
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-square btn-ghost btn-sm">✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<form method="POST" use:enhance>
|
||||||
|
<TextInput bordered name="name">
|
||||||
|
{#snippet label()}
|
||||||
|
<UserRound size="18" />
|
||||||
|
{messages.residents_modal_label_name()}
|
||||||
|
{/snippet}
|
||||||
|
</TextInput>
|
||||||
|
<TextInput bordered name="phoneNumber" type={'tel'}>
|
||||||
|
{#snippet label()}
|
||||||
|
<Phone size="18" />
|
||||||
|
{messages.residents_modal_label_phone()}
|
||||||
|
{/snippet}
|
||||||
|
</TextInput>
|
||||||
|
<ModalActions>
|
||||||
|
<Button type="submit" block 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" transition:fade>
|
||||||
|
<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} />
|
||||||
|
</div>
|
||||||
Loading…
Add table
Reference in a new issue
Errors could be an enum, to be used by the frontend