Skip to content

Commit

Permalink
fix: forms -d
Browse files Browse the repository at this point in the history
  • Loading branch information
arpowers committed Sep 11, 2024
1 parent ab309e8 commit 4e4aae6
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 110 deletions.
21 changes: 11 additions & 10 deletions @fiction/cards/contact/ElCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,24 @@ const isVisible = vue.ref(false)
vue.onMounted(async () => {
await useElementVisible({ selector: `.minimal-profile`, onVisible: () => isVisible.value = true })
})
vue.onMounted(() => {
// Load the Typeform embed script
const script = document.createElement('script')
script.src = '//embed.typeform.com/next/embed.js'
script.async = true
document.body.appendChild(script)
})
</script>

<template>
<div class="minimal-profile" :class="card.classes.value.contentWidth">
<div class="text-center">
<div class="md:inline-flex gap-6 lg:gap-16 justify-center" :class="uc.layout === 'left' ? 'md:flex-row-reverse' : ''">
<div class="w-full md:w-[40vw] px-2">
<div class="overflow-hidden relative border border-theme-200 dark:border-theme-700 rounded-xl h-full bg-theme-50 dark:bg-theme-800/50">
<CardForm :card form-template-id="contact" class="h-full w-full" />
<div v-if="card.site" class="overflow-hidden relative border border-theme-200 dark:border-theme-700 rounded-xl h-full bg-theme-50 dark:bg-theme-800/50">
<CardForm
:site="card.site"
:config="{
formTemplateId: 'contact',
userConfig: {
notifyEmails: card.userConfig.value.notifyEmails,
},
}"
class="h-full w-full"
/>
</div>
</div>
<div class="md:w-[50%] mt-6 md:mt-0 flex items-center text-left">
Expand Down
43 changes: 27 additions & 16 deletions @fiction/cards/contact/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { vue } from '@fiction/core'
import { FormUserConfigSchema } from '@fiction/forms'
import { CardTemplate } from '@fiction/site/card'
import { InputOption } from '@fiction/ui'
import { z } from 'zod'
import { standardOption } from '../inputSets'

const templateId = 'contact'

export const UserConfigSchema = z.object({
layout: z.enum(['left', 'right']).optional().describe('Layout of the card, image on left or right'),
title: z.string().optional().describe('Primary headline for profile 3 to 8 words'),
subTitle: z.string().optional().describe('Formatted markdown of profile with paragraphs, 30 to 60 words, 2 paragraphs'),
superTitle: z.string().optional().describe('Shorter badge above headline, 2 to 5 words'),
items: z.array(z.object({
title: z.string().optional().describe('Title for list of details'),
items: z.array(z.object({
Expand All @@ -25,29 +23,44 @@ export const UserConfigSchema = z.object({
href: z.string().optional().describe('Full link for href'),
icon: z.string().optional().describe('icon reference associated with the social media platform (x, youtube, facebook, etc)'),
})).optional().describe('List of social media links'),
form: FormUserConfigSchema.optional(),
notifyEmails: z.array(z.object({ email: z.string().email() })).optional().describe('List of emails to notify when form is submitted'),
})

export type UserConfig = z.infer<typeof UserConfigSchema>

const options = [
standardOption.ai(),
]
new InputOption({ key: 'layout', label: 'Layout', input: 'InputSelect', list: ['left', 'right'], description: 'Layout of the card, image on left or right' }),
new InputOption({ key: 'items', label: 'Contact Details', input: 'InputList', options: [
new InputOption({ key: 'title', label: 'Title', input: 'InputText' }),
new InputOption({ key: 'items', label: 'Details', input: 'InputList', options: [
new InputOption({ key: 'title', label: 'Title', input: 'InputText' }),
new InputOption({ key: 'content', label: 'Content', input: 'InputText' }),
new InputOption({ key: 'icon', label: 'Icon', input: 'InputIcon' }),
new InputOption({ key: 'href', label: 'Link', input: 'InputText' }),
] }),
] }),
new InputOption({ key: 'socials', label: 'Social Media', input: 'InputList', options: [
new InputOption({ key: 'name', label: 'Name', input: 'InputText' }),
new InputOption({ key: 'href', label: 'Link', input: 'InputText' }),
new InputOption({ key: 'icon', label: 'Icon', input: 'InputIcon' }),
] }),
new InputOption({ key: 'notifyEmails', label: 'Notify Email', input: 'InputList', options: [
new InputOption({ key: 'email', label: 'Email', input: 'InputEmail' }),
] }),
] as InputOption[]

const defaultContent: UserConfig = {
superTitle: 'Contact',
title: 'Get In Touch',
subTitle: `Send me a message and I'll respond within 24 hours.`,
items: [
{
title: 'Chat / Email',
title: 'Message',
items: [
{ title: 'Send me an email', content: 'test@example.com', href: 'mailto:test@example.com', icon: 'i-tabler-mail' },
{ title: 'Discord Community', content: 'Join', href: '#', icon: 'i-tabler-brand-discord' },
{ title: 'Way To Get In Touch', content: 'test@example.com', href: 'mailto:test@example.com', icon: 'i-tabler-mail' },
{ title: 'Another Way', content: 'Join', href: '#', icon: 'i-tabler-brand-discord' },
],
},
{
title: 'Phone',
title: 'Call',
items: [
{ title: '+1(888) 888-8888', content: '', href: '#', icon: 'i-tabler-phone' },
],
Expand All @@ -60,9 +73,7 @@ const defaultContent: UserConfig = {
{ name: '@handle on linkedin', href: '#', icon: 'linkedin' },
],

form: {
notifyEmails: ['arpowers@gmail.com', 'andrew@fiction.com'],
},
notifyEmails: [{ email: 'andrew@fiction.com' }],
}

export const templates = [
Expand All @@ -74,7 +85,7 @@ export const templates = [
colorTheme: 'blue',
el: vue.defineAsyncComponent(async () => import('./ElCard.vue')),
getUserConfig: () => defaultContent,
isPublic: false,
isPublic: true,
options,
schema: UserConfigSchema,
demoPage: async () => {
Expand Down
22 changes: 2 additions & 20 deletions @fiction/cards/test/tpl.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,27 +424,9 @@ describe('verify template settings config', async () => {
},
{
"hasDemo": true,
"isPublic": false,
"isPublic": true,
"templateId": "contact",
"unusedSchema": {
"form": "object",
"form.notifyEmails": "array",
"items": "array, List of contact details",
"items.0.items": "array, List of details with contact details, location, etc.",
"items.0.items.0.content": "string",
"items.0.items.0.href": "string",
"items.0.items.0.icon": "string",
"items.0.items.0.title": "string",
"items.0.title": "string, Title for list of details",
"layout": "string, Layout of the card, image on left or right",
"socials": "array, List of social media links",
"socials.0.href": "string, Full link for href",
"socials.0.icon": "string, icon reference associated with the social media platform (x, youtube, facebook, etc)",
"socials.0.name": "string, @handle on (platform)",
"subTitle": "string, Formatted markdown of profile with paragraphs, 30 to 60 words, 2 paragraphs",
"superTitle": "string, Shorter badge above headline, 2 to 5 words",
"title": "string, Primary headline for profile 3 to 8 words",
},
"unusedSchema": {},
},
{
"hasDemo": true,
Expand Down
5 changes: 5 additions & 0 deletions @fiction/core/plugin-env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ export class FictionEnv<

setupWindowKeyListeners(): void {
const emitKeypress = (event: BrowserEventObject<'keydown' | 'keyup'>): void => {
if (!event.key) {
this.log.warn('keypress event missing key', { data: event })
return
}

const direction = event.type === 'keydown' ? 'down' : 'up'
this.events.emit('keypress', { key: event.key.toLowerCase(), direction })
}
Expand Down
64 changes: 29 additions & 35 deletions @fiction/forms/deck/CardForm.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
<script lang="ts" setup>
import { useService, vue, waitFor } from '@fiction/core'
import XButton from '@fiction/ui/buttons/XButton.vue'
import ElForm from '@fiction/ui/inputs/ElForm.vue'
import El404 from '@fiction/ui/page/El404.vue'
import type { Card } from '@fiction/site'
import type { Card, Site } from '@fiction/site'
import { loadForm } from '../utils/load.js'
import FormLoading from './FormLoading.vue'
import FormProgressBar from './FormProgressBar.vue'
import type { FictionForms } from '..'
import type { FictionForms, FormConfigPortable } from '..'
import type { Form } from '../form'
const props = defineProps({
card: { type: Object as vue.PropType<Card>, required: true },
formTemplateId: { type: String, default: '' },
formId: { type: String, default: '' },
})
const { site, config } = defineProps<{ site: Site, config: FormConfigPortable }>()
const { fictionForms } = useService<{ fictionForms: FictionForms }>()
Expand All @@ -22,19 +20,11 @@ vue.onMounted(async () => {
await waitFor(500)
try {
const site = props.card.site
if (!site) {
throw new Error('site not found')
}
const { formTemplateId, formId } = props
if (!formTemplateId && !formId) {
throw new Error('formTemplateId or formId not found')
}
form.value = await loadForm({ site, formTemplateId, fictionForms })
form.value = await loadForm({ site, fictionForms, config })
}
catch (e) {
console.error(e)
Expand All @@ -52,11 +42,14 @@ const showNavigation = vue.computed(() => !form.value?.submittedData.value)
</script>

<template>
<div
<ElForm
v-if="form"
class="card-deck-theme theme-wrap theme-font overflow-hidden bg-cover relative"
:data-value="JSON.stringify(form.formValues.value || {})"
:data-submitted="JSON.stringify(form.submittedData.value || {})"
:data="form.formValues.value"
@submit="form.nextCard()"
@update:valid="form.setCurrentCardValid($event)"
>
<FormProgressBar :progress="form.percentComplete.value" />
<transition
Expand Down Expand Up @@ -93,26 +86,27 @@ const showNavigation = vue.computed(() => !form.value?.submittedData.value)
</div>
</transition>
<div v-if="form && showNavigation" class="gap-2 navigation absolute right-4 bottom-4 flex justify-center items-center z-30">
<button
:disabled="!form.isPrevAvailable"
class=" disabled:opacity-50 text-theme-300 dark:text-theme-600 flex"
@click="form?.prevCard()"
>
<span class="sr-only">Previous</span>
<span class="i-tabler-arrow-up text-xl" />
</button>
<div v-if="form && showNavigation" class="gap-2 navigation absolute bottom-0 right-0 p-4 flex justify-between items-center z-30">
<XButton
:disabled="!form.isPrevAvailable.value"
:data-active-index="form.activeCardIndex.value"
size="xs"
rounding="full"
class="disabled:opacity-50 text-theme-300 dark:text-theme-500 flex"
icon="i-tabler-arrow-up"
@click.prevent="form?.prevCard()"
/>
<button
:disabled="!form.isNextAvailable"
class="disabled:opacity-50 text-theme-300 dark:text-theme-400 flex"
@click="form?.nextCard()"
>
<span class="sr-only">Next</span>
<span class="i-tabler-arrow-down text-xl" />
</button>
<XButton
:disabled="!form.isNextAvailable.value"
size="xs"
rounding="full"
class="disabled:opacity-50 text-theme-300 dark:text-theme-500 flex"
type="submit"
icon="i-tabler-arrow-down"
/>
</div>
</div>
</ElForm>
</template>
<style lang="less">
Expand Down
7 changes: 2 additions & 5 deletions @fiction/forms/deck/InputWrap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,10 @@ function handleValidChange(valid: boolean) {
:class="contentClasses"
>
<transition :name="form?.slideTransition.value" mode="out-in">
<ElForm
<div
:id="ic?.cardId"
:key="ic?.cardId"
class="no-scrollbar overflow-y-auto"
:data="form.formValues.value"
@submit="submitCard()"
@update:valid="handleValidChange"
>
<div class="mx-auto w-full h-full max-w-4xl px-8 @md:px-[4em] py-12 @lg:py-[15vh]">
<div class="relative" :data-card-id="card.cardId">
Expand Down Expand Up @@ -211,7 +208,7 @@ function handleValidChange(valid: boolean) {
</div>
</div>
</div>
</ElForm>
</div>
</transition>
</div>
</div>
Expand Down
12 changes: 6 additions & 6 deletions @fiction/forms/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract class FormQuery extends Query<FormSubmissionSettings> {

export type WhereSubmission = { formId?: string, formTemplateId?: string, orgId?: string, submissionId?: string } & ({ orgId: string, formTemplateId: string } | { formId: string } | { submissionId: string })

export type SubmissionCreate = { fields: Partial<FormSubmissionConfig> }
export type SubmissionCreate = { fields: Partial<FormSubmissionConfig> & FormConfigPortable }

export type ManageSubmissionRequest =
| { _action: 'create', orgId: string } & SubmissionCreate
Expand Down Expand Up @@ -170,9 +170,9 @@ export class QueryManageSubmission extends FormQuery {
private async sendUserEmailNotification(params: SubmissionParams & { _action: 'create' }, meta: EndpointMeta): Promise<EndpointResponse<EmailResponse[]>> {
const { orgId, fields } = params
try {
const { title, card } = fields
const { title, userConfig = {}, card } = fields

const { notifyEmails } = fields.card?.userConfig || {}
const { notifyEmails } = userConfig
// Retrieve organization details
const r = await this.settings.fictionUser?.queries.ManageOrganization.serve({
_action: 'retrieve',
Expand Down Expand Up @@ -205,10 +205,10 @@ export class QueryManageSubmission extends FormQuery {
}

// limit to 5 emails
const emailList = notifyEmails?.slice(0, 5) || [orgEmail]
const emailList = notifyEmails?.slice(0, 5) || [{ email: orgEmail }]

const emailPromises = emailList.map(to => this.settings.fictionEmail.renderAndSendEmail({
to,
const emailPromises = emailList.map(item => this.settings.fictionEmail.renderAndSendEmail({
to: item.email,
subject: heading,
bodyMarkdown,
heading,
Expand Down
27 changes: 18 additions & 9 deletions @fiction/forms/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,22 @@ export class Form extends FictionObject<FormSettings> {
}

toConfig(): FormConfigPortable {
const card = this.card.value.toConfig()
return { formId: this.settings.formId, card }
return {
formId: this.settings.formId,
formTemplateId: this.settings.formTemplateId,
orgId: this.settings.orgId,
title: this.settings.title,
userConfig: this.settings.userConfig,
card: this.card.value.toConfig(),
status: this.settings.status,
}
}

toSubmissionData() {
return {
...this.toConfig(),
userValues: this.formValues.value,
}
}

async submitForm() {
Expand All @@ -187,16 +201,11 @@ export class Form extends FictionObject<FormSettings> {
if (!orgId) {
throw new Error('submitForm: orgId is required')
}
const { formId, formTemplateId } = this.settings

const r = await this.settings.fictionForms.requests.ManageSubmission.request({
_action: 'create',
orgId,
fields: {
formId,
formTemplateId,
card: this.card.value.toConfig(),
userValues: this.formValues.value,
},
fields: this.toSubmissionData(),
})

if (r.status === 'success') {
Expand Down
Loading

0 comments on commit 4e4aae6

Please sign in to comment.