Chapter 3, Improving the Contact Form.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Contact" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"message" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
@@ -14,3 +14,11 @@ model Post {
|
|||||||
body String
|
body String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Contact {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
email String
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|||||||
30
api/src/graphql/contacts.sdl.js
Normal file
30
api/src/graphql/contacts.sdl.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export const schema = gql`
|
||||||
|
type Contact {
|
||||||
|
id: Int!
|
||||||
|
name: String!
|
||||||
|
email: String!
|
||||||
|
message: String!
|
||||||
|
createdAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
contacts: [Contact!]! @requireAuth
|
||||||
|
contact(id: Int!): Contact @requireAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateContactInput {
|
||||||
|
name: String!
|
||||||
|
email: String!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateContactInput {
|
||||||
|
name: String
|
||||||
|
email: String
|
||||||
|
message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createContact(input: CreateContactInput!): Contact! @skipAuth
|
||||||
|
}
|
||||||
|
`
|
||||||
17
api/src/services/contacts/contacts.js
Normal file
17
api/src/services/contacts/contacts.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
|
export const contacts = () => {
|
||||||
|
return db.contact.findMany()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const contact = ({ id }) => {
|
||||||
|
return db.contact.findUnique({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createContact = ({ input }) => {
|
||||||
|
return db.contact.create({
|
||||||
|
data: input,
|
||||||
|
})
|
||||||
|
}
|
||||||
6
api/src/services/contacts/contacts.scenarios.js
Normal file
6
api/src/services/contacts/contacts.scenarios.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const standard = defineScenario({
|
||||||
|
contact: {
|
||||||
|
one: { data: { name: 'String', email: 'String', message: 'String' } },
|
||||||
|
two: { data: { name: 'String', email: 'String', message: 'String' } },
|
||||||
|
},
|
||||||
|
})
|
||||||
54
api/src/services/contacts/contacts.test.js
Normal file
54
api/src/services/contacts/contacts.test.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
contacts,
|
||||||
|
contact,
|
||||||
|
createContact,
|
||||||
|
updateContact,
|
||||||
|
deleteContact,
|
||||||
|
} from './contacts'
|
||||||
|
|
||||||
|
// Generated boilerplate tests do not account for all circumstances
|
||||||
|
// and can fail without adjustments, e.g. Float and DateTime types.
|
||||||
|
// Please refer to the RedwoodJS Testing Docs:
|
||||||
|
// https://redwoodjs.com/docs/testing#testing-services
|
||||||
|
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
||||||
|
|
||||||
|
describe('contacts', () => {
|
||||||
|
scenario('returns all contacts', async (scenario) => {
|
||||||
|
const result = await contacts()
|
||||||
|
|
||||||
|
expect(result.length).toEqual(Object.keys(scenario.contact).length)
|
||||||
|
})
|
||||||
|
|
||||||
|
scenario('returns a single contact', async (scenario) => {
|
||||||
|
const result = await contact({ id: scenario.contact.one.id })
|
||||||
|
|
||||||
|
expect(result).toEqual(scenario.contact.one)
|
||||||
|
})
|
||||||
|
|
||||||
|
scenario('creates a contact', async () => {
|
||||||
|
const result = await createContact({
|
||||||
|
input: { name: 'String', email: 'String', message: 'String' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.name).toEqual('String')
|
||||||
|
expect(result.email).toEqual('String')
|
||||||
|
expect(result.message).toEqual('String')
|
||||||
|
})
|
||||||
|
|
||||||
|
scenario('updates a contact', async (scenario) => {
|
||||||
|
const original = await contact({ id: scenario.contact.one.id })
|
||||||
|
const result = await updateContact({
|
||||||
|
id: original.id,
|
||||||
|
input: { name: 'String2' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.name).toEqual('String2')
|
||||||
|
})
|
||||||
|
|
||||||
|
scenario('deletes a contact', async (scenario) => {
|
||||||
|
const original = await deleteContact({ id: scenario.contact.one.id })
|
||||||
|
const result = await contact({ id: original.id })
|
||||||
|
|
||||||
|
expect(result).toEqual(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,21 +1,76 @@
|
|||||||
import { MetaTags } from '@redwoodjs/web'
|
import { MetaTags, useMutation } from '@redwoodjs/web'
|
||||||
import { Form, TextField, Submit, TextAreaField } from '@redwoodjs/forms'
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
import {
|
||||||
|
FieldError,
|
||||||
|
Form,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
Submit,
|
||||||
|
TextAreaField,
|
||||||
|
} from '@redwoodjs/forms'
|
||||||
|
|
||||||
|
const CREATE_CONTACT = gql`
|
||||||
|
mutation CreateContactMutation($input: CreateContactInput!) {
|
||||||
|
createContact(input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const ContactPage = () => {
|
const ContactPage = () => {
|
||||||
|
const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
|
||||||
|
onCompleted: () => {
|
||||||
|
toast.success('Thank you for your submission!')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const onSubmit = (data) => {
|
const onSubmit = (data) => {
|
||||||
|
create({ variables: { input: data } })
|
||||||
console.log(data)
|
console.log(data)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetaTags title="Contact" description="Contact page" />
|
<MetaTags title="Contact" description="Contact page" />
|
||||||
<Form onSubmit={onSubmit}>
|
|
||||||
<label htmlFor="name">Name</label>
|
<Toaster />
|
||||||
<TextField name="name" validation={{required: true}} />
|
<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }}>
|
||||||
<label htmlFor="email">Email</label>
|
<Label name="name" errorClassName="error">
|
||||||
<TextField name="email" validation={{required: true}} />
|
Name
|
||||||
<label htmlFor="message">Message</label>
|
</Label>
|
||||||
<TextAreaField name="message" validation={{required: true}} />
|
<TextField
|
||||||
<Submit>Save</Submit>
|
name="name"
|
||||||
|
validation={{ required: true }}
|
||||||
|
errorClassName="error"
|
||||||
|
/>
|
||||||
|
<FieldError name="name" className="error" />
|
||||||
|
|
||||||
|
<Label name="email" errorClassName="error">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="email"
|
||||||
|
validation={{
|
||||||
|
required: true,
|
||||||
|
pattern: {
|
||||||
|
value: /^[^@]+@[^.]+\..+$/,
|
||||||
|
message: 'Please enter a valid email address',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
errorClassName="error"
|
||||||
|
/>
|
||||||
|
<FieldError name="email" className="error" />
|
||||||
|
|
||||||
|
<Label name="message" errorClassName="error">
|
||||||
|
Message
|
||||||
|
</Label>
|
||||||
|
<TextAreaField
|
||||||
|
name="message"
|
||||||
|
validation={{ required: true }}
|
||||||
|
errorClassName="error"
|
||||||
|
/>
|
||||||
|
<FieldError name="message" className="error" />
|
||||||
|
|
||||||
|
<Submit disabled={loading}>Save</Submit>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user