Last active
November 23, 2023 21:15
-
-
Save waldothedeveloper/60801203d508188b57bc4701454e5671 to your computer and use it in GitHub Desktop.
create-services.tsx
This file contains hidden or 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
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