diff --git a/api/db/migrations/20220421213019_create_contact/migration.sql b/api/db/migrations/20220421213019_create_contact/migration.sql
new file mode 100644
index 0000000..8d7bd91
--- /dev/null
+++ b/api/db/migrations/20220421213019_create_contact/migration.sql
@@ -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
+);
diff --git a/api/db/schema.prisma b/api/db/schema.prisma
index 3e7e700..7c6c24a 100644
--- a/api/db/schema.prisma
+++ b/api/db/schema.prisma
@@ -14,3 +14,11 @@ model Post {
body String
createdAt DateTime @default(now())
}
+
+model Contact {
+ id Int @id @default(autoincrement())
+ name String
+ email String
+ message String
+ createdAt DateTime @default(now())
+}
diff --git a/api/src/graphql/contacts.sdl.js b/api/src/graphql/contacts.sdl.js
new file mode 100644
index 0000000..d7b70e5
--- /dev/null
+++ b/api/src/graphql/contacts.sdl.js
@@ -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
+ }
+`
diff --git a/api/src/services/contacts/contacts.js b/api/src/services/contacts/contacts.js
new file mode 100644
index 0000000..a36cf89
--- /dev/null
+++ b/api/src/services/contacts/contacts.js
@@ -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,
+ })
+}
diff --git a/api/src/services/contacts/contacts.scenarios.js b/api/src/services/contacts/contacts.scenarios.js
new file mode 100644
index 0000000..64885cf
--- /dev/null
+++ b/api/src/services/contacts/contacts.scenarios.js
@@ -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' } },
+ },
+})
diff --git a/api/src/services/contacts/contacts.test.js b/api/src/services/contacts/contacts.test.js
new file mode 100644
index 0000000..814a53e
--- /dev/null
+++ b/api/src/services/contacts/contacts.test.js
@@ -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)
+ })
+})
diff --git a/web/src/pages/ContactPage/ContactPage.js b/web/src/pages/ContactPage/ContactPage.js
index 0d784f9..be00375 100644
--- a/web/src/pages/ContactPage/ContactPage.js
+++ b/web/src/pages/ContactPage/ContactPage.js
@@ -1,21 +1,76 @@
-import { MetaTags } from '@redwoodjs/web'
-import { Form, TextField, Submit, TextAreaField } from '@redwoodjs/forms'
+import { MetaTags, useMutation } from '@redwoodjs/web'
+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 [create, { loading, error }] = useMutation(CREATE_CONTACT, {
+ onCompleted: () => {
+ toast.success('Thank you for your submission!')
+ },
+ })
+
const onSubmit = (data) => {
+ create({ variables: { input: data } })
console.log(data)
}
return (
<>