Skip to content

Instantly share code, notes, and snippets.

@waldothedeveloper
Last active November 23, 2023 21:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save waldothedeveloper/60801203d508188b57bc4701454e5671 to your computer and use it in GitHub Desktop.
Save waldothedeveloper/60801203d508188b57bc4701454e5671 to your computer and use it in GitHub Desktop.
create-services.tsx
import { conform, useForm } from '@conform-to/react'
import { getFieldsetConstraint, parse } from '@conform-to/zod'
import type { Prisma } from '@prisma/client'
import { json, redirect, type DataFunctionArgs } from '@remix-run/node'
import { Form, useActionData, useLoaderData } from '@remix-run/react'
import { useId } from 'react'
import { z } from 'zod'
import ServicesForm from '~/components/onboarding/services-form'
import { checkUserID } from '~/utils/auth.server'
import { prisma } from '~/utils/db.server'
type addOnType = {
addOn: string
addOnPrice: string
id: string
}
const ServicesSchema = z
.object({
title: z
.string({
required_error: 'A service title is required',
invalid_type_error: 'The service title must be a string',
})
.min(2, {
message:
'The service title must be at least 2 characters or a single word.',
})
.max(50, {
message: 'The service title cannot be more than 50 characters.',
})
.trim(),
description: z
.string({
required_error: 'A service description is required',
invalid_type_error: 'The service description must be a string',
})
.min(1, {
message: 'The service description must be at least 50 characters.',
})
.max(250, {
message: 'The service title cannot be more than 50 characters.',
})
.trim(),
price: z
.number({
required_error: 'A service price is required',
})
.positive()
.nonnegative()
.min(1, {
message: 'The service price must be at least $1 dollar.',
}),
category: z.string().optional(),
custom_category: z.string().optional(),
location: z.array(z.string()).nonempty(),
/*
remember to add the types the right way below
ref: https://github.com/JacobWeisenburger/zod_utilz/blob/4093595e5a6d95770872598ba3bc405d4e9c963b/src/stringToJSON.ts#LL4-L12C8
: z.infer<ReturnType<typeof json>>
*/
addOn: z.string().transform((str, ctx) => {
try {
const addOns = JSON.parse(str) as addOnType[]
const totalAddOnPrice = addOns.reduce((acc, curr) => {
return acc + parseFloat(curr.addOnPrice)
}, 0)
// limit the total addOnPrice to 50,000
if (totalAddOnPrice > 50000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'The total add-on price cannot be more than $50,000.',
path: ['addOn'],
})
return null
} else {
return str
}
} catch (e) {
ctx.addIssue({
code: 'custom',
message: 'Invalid JSON',
path: ['addOn'],
})
return null
}
}),
})
.superRefine((schema, ctx) => {
const { category, custom_category } = schema
if (!category && !custom_category) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Please select a category above or create a custom category.',
path: ['custom_category'],
})
return z.NEVER
} else {
return schema
}
})
export async function loader(args: DataFunctionArgs) {
const userId = await checkUserID(args)
if (!userId) {
return redirect('/')
}
const categories = await prisma.category.findMany({
orderBy: {
name: 'asc',
},
})
return json({ categories })
}
// submitting and validating the form
export async function action(args: DataFunctionArgs) {
const userId = await checkUserID(args)
if (!userId) {
return redirect('/')
}
const formData = await args.request.formData()
const submission = parse(formData, { schema: ServicesSchema })
if (!submission.value || submission.intent !== 'submit') {
return json({
...submission,
error: { '': ['THIS SHOULD BE SENT TO THE CLIENT...BUT IS NOT'] },
})
}
/*
save to database
then redirect to next page
ref: https://stackoverflow.com/questions/149055/how-to-format-numbers-as-currency-strings
Please, to anyone reading this in the future, do not use float to store currency. You will loose precision and data. You should store it as a integer number of cents (or pennies etc.) and then convert prior to output. –
Philip Whitehouse
Mar 4, 2012 at 13:35
*/
return json(submission)
// return redirect('/onboarding/get-paid')
}
export default function CreateServices() {
const data = useLoaderData<typeof loader>() as Prisma.JsonObject
const id = useId()
const lastSubmission = useActionData<typeof action>()
const [form, fields] = useForm({
id,
shouldValidate: 'onBlur',
shouldRevalidate: 'onBlur',
constraint: getFieldsetConstraint(ServicesSchema),
lastSubmission,
onValidate({ formData }) {
return parse(formData, { schema: ServicesSchema })
},
defaultValue: {
title: '',
description: '',
price: '',
category: '',
custom_category: '',
},
})
console.log(form.errors)
return (
<div className="container mx-auto px-6 py-24 sm:px-24">
<div className="pb-24">
<div>
<div className="mx-auto max-w-2xl lg:mx-0">
<h2 className="text-4xl font-bold tracking-tight text-gray-900">
Services
</h2>
<p className="mt-6 text-lg leading-8 text-gray-600">
Your store can sell one or multiple services in different
categories and pricing tiers. Feel free to start with just one for
now, you can always add more services later.
</p>
</div>
</div>
</div>
<div className="space-x-4">
<div>
<Form {...form.props} method="post">
<ServicesForm conform={conform} fields={fields} categories={data} />
<div className="mt-6 flex items-center justify-end gap-x-6">
<button
type="button"
className="text-sm font-semibold leading-6 text-slate-900"
>
Cancel
</button>
<button
type="submit"
className="rounded-md bg-cyan-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600"
>
Save
</button>
</div>
</Form>
</div>
</div>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment