-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implementation of volunteers including some input and form helpers
- Loading branch information
Showing
7 changed files
with
296 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import html from "../../server/templates"; | ||
import { BaseLiveViewComponent, LiveViewChangeset, LiveViewExternalEventListener, LiveViewMountParams, LiveViewSocket, StringPropertyValues } from "../../server/types"; | ||
import { SessionData } from "express-session"; | ||
import { Volunteer, changeset, create_volunteer } from "./data"; | ||
import { submit } from "../../server/templates/helpers/submit"; | ||
import { form_for } from "../../server/templates/helpers/form_for"; | ||
import { error_tag, telephone_input, text_input } from "../../server/templates/helpers/inputs"; | ||
|
||
export interface VolunteerContext { | ||
volunteers: Volunteer[] | ||
changeset: LiveViewChangeset<Volunteer> | ||
} | ||
|
||
export class VolunteerComponent extends BaseLiveViewComponent<VolunteerContext, unknown> | ||
implements LiveViewExternalEventListener<VolunteerContext, "save", Volunteer> { | ||
|
||
mount(params: LiveViewMountParams, session: Partial<SessionData>, socket: LiveViewSocket<VolunteerContext>) { | ||
return { | ||
volunteers: [], | ||
changeset: changeset({}, {}) | ||
} | ||
}; | ||
|
||
render(context: VolunteerContext) { | ||
const { changeset, volunteers } = context; | ||
return html` | ||
<h1>Volunteer Check-In</h1> | ||
<div id="checkin"> | ||
${form_for<Volunteer>("#", { phx_submit: "save" })} | ||
<div class="field"> | ||
${text_input<Volunteer>(changeset, "name", { placeholder: "Name", autocomplete: "off" })} | ||
${error_tag(changeset, "name")} | ||
</div> | ||
<div class="field"> | ||
${telephone_input<Volunteer>(changeset, "phone", { placeholder: "Phone", autocomplete: "off" })} | ||
${error_tag(changeset, "phone")} | ||
</div> | ||
${submit("Check In", { phx_disable_with: "Saving..." })} | ||
</form> | ||
<div id="volunteers" phx-update="prepend"> | ||
${volunteers.map(this.renderVolunteer)} | ||
</div> | ||
</div> | ||
` | ||
}; | ||
|
||
renderVolunteer(volunteer: Volunteer) { | ||
return html` | ||
<div id="${volunteer.id}" class="volunteer ${volunteer.checked_out ? "out" : ""}"> | ||
<div class="name"> | ||
${volunteer.name} | ||
</div> | ||
<div class="phone"> | ||
📞 ${volunteer.phone} | ||
</div> | ||
<div class="status"> | ||
${volunteer.checked_out ? "☑️ Volunteer" : html`<button>Check Out</button>`} | ||
</div> | ||
</div> | ||
` | ||
} | ||
|
||
handleEvent(event: "save", params: StringPropertyValues<Pick<Volunteer, "name" | "phone">>, socket: LiveViewSocket<VolunteerContext>): VolunteerContext { | ||
const { volunteers } = socket.context; | ||
console.log("save", params); | ||
const volunteer: Partial<Volunteer> = { | ||
name: params.name, | ||
phone: params.phone, | ||
} | ||
const createChangeset = create_volunteer(volunteer); | ||
if (createChangeset.valid) { | ||
const newVolunteer = createChangeset.data as Volunteer; | ||
const newVolunteers = [newVolunteer, ...volunteers]; | ||
const newChangeset = changeset({}, {}); | ||
return { | ||
volunteers: newVolunteers, | ||
changeset: newChangeset | ||
} | ||
} else { | ||
return { | ||
volunteers, | ||
changeset: createChangeset | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
|
||
import { Volunteer, VolunteerSchema } from './data'; | ||
|
||
describe("test volunteers", () => { | ||
it("valid volunteer", () => { | ||
const volunteer = { | ||
id: '1', | ||
name: 'John Doe', | ||
phone: '123-456-7890', | ||
checked_out: false | ||
} as Volunteer; | ||
expect(VolunteerSchema.safeParse(volunteer).success).toBe(true); | ||
}); | ||
|
||
it("invalid volunteer, name too short", () => { | ||
const volunteer = { | ||
id: '1', | ||
name: 'J', | ||
phone: '123-456-7890', | ||
checked_out: false | ||
} as Volunteer; | ||
const result = VolunteerSchema.safeParse(volunteer); | ||
expect(result.success).toBe(false); | ||
if (result.success === false) { | ||
const formatted = result.error.format(); | ||
expect(result.error.isEmpty).toBe(false); | ||
expect(result.error.issues.length).toBe(1); | ||
expect(result.error.issues[0].message).toBe('Should be at least 2 characters'); | ||
} | ||
}); | ||
|
||
it("invalid volunteer, phone invalid", () => { | ||
const volunteer = { | ||
id: '1', | ||
name: 'John Doe', | ||
phone: '123', | ||
checked_out: false | ||
} as Volunteer; | ||
const result = VolunteerSchema.safeParse(volunteer); | ||
expect(result.success).toBe(false); | ||
if (result.success === false) { | ||
expect(result.error.isEmpty).toBe(false); | ||
expect(result.error.issues.length).toBe(1); | ||
expect(result.error.issues[0].message).toBe('Should be a valid phone number'); | ||
} | ||
}); | ||
|
||
it("valid volunteer, default id, checkout out", () => { | ||
const volunteer = { | ||
name: 'Jane Doe', | ||
phone: '123-456-7890', | ||
} as Volunteer; | ||
const result = VolunteerSchema.safeParse(volunteer); | ||
expect(result.success).toBe(true); | ||
}); | ||
|
||
it("invalid volunteer, phone and name invalid", () => { | ||
const volunteer = { | ||
name: 'J', | ||
phone: '123', | ||
} as Volunteer; | ||
const result = VolunteerSchema.safeParse(volunteer); | ||
expect(result.success).toBe(false); | ||
if (result.success === false) { | ||
expect(result.error.isEmpty).toBe(false); | ||
expect(result.error.issues.length).toBe(2); | ||
const formatted = result.error.format(); | ||
expect(formatted.name).not.toBeUndefined() | ||
expect(formatted.phone).not.toBeUndefined() | ||
} | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { z } from 'zod'; | ||
import { nanoid } from 'nanoid'; | ||
import { updatedDiff } from 'deep-object-diff' | ||
import { LiveViewChangeset, LiveViewChangesetErrors } from '../../server/types'; | ||
|
||
const phoneRegex = /^\d{3}[\s-.]?\d{3}[\s-.]?\d{4}$/ | ||
|
||
// Use Zod to define the schema for the Volunteer model | ||
// More on Zod - https://github.com/colinhacks/zod | ||
export const VolunteerSchema = z.object({ | ||
id: z.string().default(nanoid()), | ||
name: z.string().min(2).max(100), | ||
phone: z.string().regex(phoneRegex, 'Should be a valid phone number'), | ||
checked_out: z.boolean().default(false), | ||
}) | ||
|
||
// infer the Volunteer model from the Zod Schema | ||
export type Volunteer = z.infer<typeof VolunteerSchema>; | ||
|
||
// in memory data store | ||
export const volunteers: Record<string, Volunteer> = {} | ||
|
||
export const changeset = (volunteer: Partial<Volunteer>, attrs: Partial<Volunteer>, action?: string): LiveViewChangeset<Volunteer> => { | ||
const merged = { ...volunteer, ...attrs }; | ||
const result = VolunteerSchema.safeParse(merged); | ||
let errors; | ||
if (result.success === false) { | ||
errors = result.error.issues.reduce((acc, issue) => { | ||
// @ts-ignore | ||
acc[issue.path[0]] = issue.message; | ||
return acc; | ||
}, {} as LiveViewChangesetErrors<Volunteer>) | ||
} | ||
return { | ||
action, | ||
changes: updatedDiff(volunteer, merged), | ||
data: result.success ? result.data : merged, | ||
valid: result.success, | ||
errors | ||
} as LiveViewChangeset<Volunteer> | ||
} | ||
|
||
export const create_volunteer = (newVolunteer: Partial<Volunteer>): LiveViewChangeset<Volunteer> => { | ||
const result = changeset({}, newVolunteer, 'create'); | ||
if (result.valid) { | ||
const v = result.data as Volunteer; | ||
volunteers[v.id] = v; | ||
} | ||
return result; | ||
} | ||
|
||
export const update_volunteer = (volunteer: Volunteer, updated: Partial<Volunteer>): LiveViewChangeset<Volunteer> => { | ||
const result = changeset(volunteer, updated, 'update'); | ||
if (result.valid) { | ||
const v = result.data as Volunteer; | ||
volunteers[v.id] = v; | ||
} | ||
return result; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import html from ".." | ||
|
||
interface FormForOptions { | ||
phx_submit?: string | ||
method?: "get" | "post" | ||
} | ||
|
||
// TODO insert hidden input for CSRF token? | ||
export const form_for = <T>(action: string, options?: FormForOptions) => { | ||
const method = options?.method ?? "post"; | ||
const phx_submit = options?.phx_submit ? `phx-submit="${options.phx_submit}"` : ""; | ||
return html` | ||
<form action="${action}" method="${method}" ${phx_submit}> | ||
` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { LiveViewChangeset } from "../../types"; | ||
import html from ".." | ||
|
||
interface InputOptions { | ||
placeholder?: string | ||
autocomplete?: "off" | "on" | ||
type?: "text" | "tel" | ||
} | ||
|
||
export const text_input = <T>(changeset: LiveViewChangeset<T>, key: keyof T, options?: InputOptions) => { | ||
const placeholder = options?.placeholder ? "placeholder=\"" + options.placeholder + "\"" : ""; | ||
const autocomplete = options?.autocomplete ? "autocomplete=\"" + options.autocomplete + "\"" : ""; | ||
const type = options?.type ?? "text"; | ||
const id = `input_${key}`; | ||
const value = changeset.data[key] ?? ""; | ||
return html` | ||
<input type="${type}" id="${id}" name="${key}" value="${value}" ${autocomplete} ${placeholder} /> | ||
` | ||
} | ||
|
||
interface TelephoneInputOptions extends Omit<InputOptions, "type"> { } | ||
|
||
export const telephone_input = <T>(changeset: LiveViewChangeset<T>, key: keyof T, options?: TelephoneInputOptions) => { | ||
return text_input(changeset, key, { ...options, type: "tel" }); | ||
} | ||
|
||
interface ErrorTagOptions { | ||
className?: string | ||
} | ||
|
||
export const error_tag = <T>(changeset: LiveViewChangeset<T>, key: keyof T, options?: ErrorTagOptions) => { | ||
const error = changeset.errors ? changeset.errors[key] : undefined; | ||
if (changeset.action && error) { | ||
const className = options?.className ?? "invalid-feedback"; | ||
return html` | ||
<span class="${className}" phx-feedback-for="${key}">${error}</span> | ||
` | ||
} | ||
return html`` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import html from ".." | ||
|
||
interface SubmitOptions { | ||
phx_disable_with: string | ||
} | ||
|
||
export const submit = (label: string, options?: SubmitOptions) => { | ||
const phx_disable_with = options?.phx_disable_with ?? ""; | ||
return html` | ||
<button ${phx_disable_with} type="submit">${label}</button> | ||
` | ||
} |