├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── (site) │ ├── api │ │ └── submit-message │ │ │ └── route.js │ ├── layout.jsx │ └── page.jsx ├── (studio) │ ├── layout.jsx │ └── studio │ │ └── [[...index]] │ │ └── page.jsx ├── favicon.ico └── globals.css ├── components └── form.jsx ├── jsconfig.json ├── lib └── utils.js ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── sanity.config.js ├── sanity ├── config │ └── sanity.client.js └── plugins │ └── inbox │ ├── components │ ├── inbox-tool.jsx │ ├── message-card.jsx │ ├── message-dialog.jsx │ └── message-list.jsx │ ├── plugin.js │ └── schema.js └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inbox Tool for Sanity Studio 2 | 3 | I've been working on a fun little project recently and thought I would share it with the community! A Sanity plugin that lets you view and manage form submissions inside a handy Studio Tool. 4 | 5 | 6 | 7 | ## Features ⚡️ 8 | 9 | - Studio Tool to view submissions. 10 | - Custom dialog to view a message and all its fields. 11 | - Star important messages. 12 | - Delete spam and unwanted messages. 13 | - New messages are moved out of unread folder once viewed. 14 | 15 | ## Installation 16 | 17 | To give this Studio Tool a try, you'll need to clone this GitHub repository locally, open it up in your code editor and then run `npm install` in the terminal to install the required dependencies. 18 | 19 | ## Environment Settings 20 | 21 | Create a `.env.local` file at the root of the project and add the following environment variables. 22 | 23 | ``` 24 | NEXT_PUBLIC_SANITY_PROJECT_ID="" 25 | NEXT_PUBLIC_SANITY_DATASET="" 26 | NEXT_PUBLIC_SANITY_API_VERSION="" 27 | SANITY_API_TOKEN="" 28 | ``` 29 | 30 | Once you have added your environment variables, open your terminal again and run `npm run dev` to start the development server. 31 | 32 | ## Usage 33 | 34 | Open your browser and navigate to `http://localhost:3000`. Here you will see a basic form, fill out the fields, hit submit and then navigate to `http://localhost:3000/studio/inbox-tool` to see your form submission. 35 | 36 | ## Important Details 37 | 38 | ### Creating documents using the HTTP API 39 | 40 | To create a document using the Sanity HTTP API requests must be authenticated with an API Token. This token should be kept safe and never exposed to the client and so we make the request in the `/api/submit-message` route handler. 41 | 42 | You can learn more about the HTTP API [here](https://www.sanity.io/docs/http-api). 43 | 44 | ### Keeping data safe 45 | 46 | By default Sanity gives unauthenticated users read access to published documents in public datasets. Our `message` document is going to contain personal information that needs to be kept private. To disable this behavior we add a `message.` prefix to the `_id` when defining our `create` mutation in `form.jsx`. This will generate a new, random, unique `_id` such as `message.s4tZYDUyXCCef1YpYu6Js5`. 47 | 48 | The important thing to know here is that documents under a sub-path (i.e. containing a `.` in the `_id`) are not publicly available and can only be read with a Token. 49 | 50 | You can learn more about IDs, paths and sub-paths [here](https://www.sanity.io/docs/ids). 51 | 52 | ```javascript 53 | // components/form.jsx 54 | 55 | const mutations = [{ 56 | create: { 57 | _id: 'message.', 58 | _type: 'message', 59 | read: false, 60 | starred: false, 61 | name: name, 62 | email: email, 63 | subject: subject, 64 | fields: [ 65 | { 66 | _key: generateID(), 67 | name: 'Occupation', 68 | value: occupation 69 | }, 70 | { 71 | _key: generateID(), 72 | name: 'Message', 73 | value: message 74 | } 75 | ] 76 | } 77 | }] 78 | ``` 79 | 80 | ## Author 81 | 82 | #### James Rea 83 | 84 | - Twitter ([@jamesreaco](https://twitter.com/jamesreaco)) 85 | - Website ([jamesrea.co](https://jamesrea.co)) 86 | 87 | For business enquiries, you can email me at hello@jamesrea.co. 88 | 89 | -------------------------------------------------------------------------------- /app/(site)/api/submit-message/route.js: -------------------------------------------------------------------------------- 1 | export async function POST(req) { 2 | 3 | const body = await req.json() 4 | 5 | const API_KEY = process.env.SANITY_API_TOKEN 6 | const PROJECT_ID = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID 7 | const DATASET = process.env.NEXT_PUBLIC_SANITY_DATASET 8 | 9 | const options = { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | Authorization: `Bearer ${API_KEY}` 14 | }, 15 | body: JSON.stringify(body) 16 | }; 17 | 18 | const url = `https://${PROJECT_ID}.api.sanity.io/v2021-06-07/data/mutate/${DATASET}` 19 | 20 | try { 21 | const response = await fetch(url, options) 22 | const data = await response.json() 23 | return new Response(JSON.stringify({ success: true, data })) 24 | } catch (error) { 25 | return new Response(JSON.stringify({ error: 'Failed to save document.' })) 26 | } 27 | } -------------------------------------------------------------------------------- /app/(site)/layout.jsx: -------------------------------------------------------------------------------- 1 | import '../globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Messages Plugin | Sanity', 8 | } 9 | 10 | export default function RootLayout({ children }) { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/(site)/page.jsx: -------------------------------------------------------------------------------- 1 | import Form from "@/components/form"; 2 | 3 | export default async function Home() { 4 | return
5 | } 6 | -------------------------------------------------------------------------------- /app/(studio)/layout.jsx: -------------------------------------------------------------------------------- 1 | import '../globals.css' 2 | 3 | export const metadata = { 4 | title: 'Inbox Plugin | Studio', 5 | } 6 | 7 | export default function RootLayout({ children }) { 8 | return ( 9 | 10 | 11 |
12 | {children} 13 |
14 | 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /app/(studio)/studio/[[...index]]/page.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { NextStudio } from 'next-sanity/studio' 3 | import config from '@/sanity.config' 4 | 5 | export default function StudioPage() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesreaco/inbox-tool-sanity/7905891c3e8cef4891706d4efccb35f1100a60c7/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /components/form.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { generateID } from '@/lib/utils'; 3 | import { useState } from 'react'; 4 | 5 | export default function Form() { 6 | 7 | const [name, setName] = useState(''); 8 | const [email, setEmail] = useState(''); 9 | const [subject, setSubject] = useState(''); 10 | const [occupation, setOccupation] = useState(''); 11 | const [message, setMessage] = useState(''); 12 | 13 | async function handleSubmit(e) { 14 | e.preventDefault() 15 | 16 | const mutations = [{ 17 | create: { 18 | _id: 'message.', 19 | _type: 'message', 20 | read: false, 21 | starred: false, 22 | name: name, 23 | email: email, 24 | subject: subject, 25 | fields: [ 26 | { 27 | _key: generateID(), 28 | name: 'Occupation', 29 | value: occupation 30 | }, 31 | { 32 | _key: generateID(), 33 | name: 'Message', 34 | value: message 35 | } 36 | ] 37 | } 38 | }] 39 | 40 | try { 41 | 42 | const response = await fetch("/api/submit-message", { 43 | method: "POST", 44 | body: JSON.stringify({ mutations }), 45 | headers: { "Content-Type": "application/json" }, 46 | }) 47 | 48 | if (response.ok) { 49 | resetUI() 50 | console.log('Document added successfully:', response) 51 | } 52 | 53 | } catch(error) { 54 | console.error('Error adding document:', error); 55 | } 56 | 57 | } 58 | 59 | function resetUI() { 60 | setEmail('') 61 | setName('') 62 | setSubject('') 63 | setOccupation('') 64 | setMessage('') 65 | } 66 | 67 | return ( 68 |
69 | 73 | setName(e.target.value)} 79 | /> 80 | setEmail(e.target.value)} 86 | /> 87 | setSubject(e.target.value)} 93 | /> 94 | setOccupation(e.target.value)} 100 | /> 101 |