32
loading...
This website collects cookies to deliver better user experience
git clone https://github.com/bathrobe/magic-next-sanity-todo-starter.git
cd magic-next-sanity-todo-starter
and then npm install
..env.local
file at the root of your project. We'll add the keys one by one.// .env.local
NEXT_PUBLIC_MAGIC_PUB_KEY=pk_test_12345
MAGIC_SECRET_KEY=sk_test_12345
ENCRYPTION_SECRET=random_encryption_string
npm install -g @sanity/cli
sanity init
from the command line and create a new one..env
file later.schemas
folder.// your-studio/schemas/todo.js
export default {
name: "todo",
title: "Todo",
type: "document",
fields: [
{
name: "text",
title: "Todo Text",
type: "string",
},
{
name: "createdAt",
title: "Created at",
type: "datetime",
},
{
name: "dueDate",
title: "Due date",
type: "datetime",
},
{
name: "isCompleted",
title: "Is completed?",
type: "boolean",
},
{
name: "completedAt",
title: "Completed At",
type: "datetime",
},
{
name: "userEmail",
title: "User Email",
type: "string",
},
],
};
schema.js
file.// your-studio/schemas/schema.js
import createSchema from "part:@sanity/base/schema-creator";
import schemaTypes from "all:part:@sanity/base/schema-type";
//...
import todo from "./todo"
export default createSchema({
name: "default",
types: schemaTypes.concat([
//...
todo
]),
});
sanity deploy
, choose a unique name for your deployed studio, and soon it should be live..env.local
.// .env.local
NEXT_PUBLIC_MAGIC_PUB_KEY=pk_test_12345
MAGIC_SECRET_KEY=sk_test_12345
ENCRYPTION_SECRET=random_encryption_string_from_earlier
NEXT_PUBLIC_SANITY_ID=your_sanity_id
NEXT_PUBLIC_SANITY_DATASET=your_sanity_dataset
SANITY_WRITE_KEY=your_sanity_write_key
npm run dev
in your project's root and test it out.useState()
hook to store the values of our submit form and todo list.react-date-picker
library. Then we'll add our states.// src/pages/todos.js
import { useState } from "react";
//we must import the datepicker's css modules manually
//so it plays nice with Next.
import DatePicker from "react-date-picker/dist/entry.nostyle";
import "react-date-picker/dist/DatePicker.css";
import "react-calendar/dist/Calendar.css";
import useAuth from "../hooks/useAuth";
import Logout from "../components/Logout";
export default function Todos() {
const { user, loading } = useAuth();
//create a state to store todoList array
const [todoList, setTodoList] = useState([]);
//create a state for the text in the todo input form
const [userInput, setUserInput] = useState("");
//create a state for the due date chosen in the datepicker
const [dueDate, setDueDate] = useState("");
//set an error message if either input is missing
const [errMessage, setErrMessage] = useState("");
//...
useAuth()
hook at the top. More information about custom hooks (as well as a helpful refresher on all things React hooks!) can be found in this Fireship video.// src/pages/todos.js
//... right after the useState hooks
//FOR THE INPUT FORM:
const handleChange = (e) => {
e.preventDefault();
setUserInput(e.target.value);
};
//FOR THE SUBMIT BUTTON:
const handleSubmit = async (e) => {
e.preventDefault();
//if either part of the form isn't filled out
//set an error message and exit
if (userInput.length == 0 || dueDate == "") {
setErrMessage("Todo text and due date must be filled out.");
} else {
//otherwise send the todo to our api
// (we'll make this next!)
await fetch("/api/todo", {
method: "POST",
body: JSON.stringify({
text: userInput,
dueDate: dueDate,
user: user.email,
}),
});
// await fetchTodos(); //(we'll add this later)
// Clear all inputs after the todo is sent to Sanity
setUserInput("");
setErrMessage("");
setDueDate("");
}
};
handleChange
stores our text input in a state. handleSubmit
first makes sure both of our fields have been filled out, then posts the todo to our serverless API route and clears out the inputs.api/todo
route and a fetchTodos()
function. Fret not! We'll get to these in the next section. For now, we'll finish rendering our form.<p>Todo app will go right here!</p>
with the form element below.// src/pages/todos.js
{/*...*/}
<form>
{/*we flex the text input and datepicker
so they display inline. */}
<div className="flex justify-center items-center">
<label for="todo" className="invisible">Your Todo</label>
<input
className="w-72 h-12 border p-4 border-blue-100"
type="text"
//our state
value={userInput}
placeholder="Make coffee."
//our function
onChange={handleChange}
/>
<div className="my-8">
<DatePicker
className="p-4"
//makes it so we cannot set due date in past
minDate={new Date()}
//our dueDate state
onChange={setDueDate}
value={dueDate}
/>
</div>
</div>{" "}
<button
className="focus:outline-none focus:ring focus:border-blue-800
px-6 py-2 rounded-xl bg-blue-500 text-blue-50 hover:bg-blue-800
font-semibold"
//our function
onClick={handleSubmit}
>
Submit
</button>
{/*error set in handleSubmit*/}
<p>{errMessage}</p>
</form>
{/*...*/}
src/pages/api/todo.js
. Since we'll want full CRUD functionality in our app, we'll need PUT and DELETE requests later. To keep our code clean, we'll use the switch
syntax for the different request types.// src/pages/api/todo.js
import client from "../../lib/sanity/client";
export default async function handler(req, res) {
switch (req.method) {
case "POST":
//this JSON arrives as a string,
//so we turn it into a JS object with JSON.parse()
const newTodo = await JSON.parse(req.body);
//then use the Sanity client to create a new todo doc
try {
await client
.create({
_type: "todo",
text: newTodo.text,
isCompleted: false,
createdAt: new Date().toISOString(),
dueDate: newTodo.dueDate,
userEmail: newTodo.user,
})
.then((res) => {
console.log(`Todo was created, document ID is ${res._id}`);
});
res
.status(200)
.json({ msg: `Todo was created, document ID is ${res._id}` });
} catch (err) {
console.error(err);
res.status(500).json({ msg: "Error, check console" });
}
break;
}
}
npm run dev
once more and open up your Sanity studio. If all went well, you should see your new todo inside of Sanity. todoList
state. We want the function to run when the page loads, whenever user data changes, and any time we alter the data (by adding, updating, or deleting todos). To manage this logic, we'll begin by adding a useEffect
hook.pages/todos.js
and importuseEffect
, as well as our Sanity client.//src/pages/todos.js
import { useState, useEffect } from "react";
// ...
import client from "../lib/sanity/client";
useState
hooks add the fetchTodos
function and tack it onto a useEffect
hook.//src/pages/todos.js
//after the useState hooks
const fetchTodos = async () => {
let fetchedTodos;
//make sure the user is loaded
if (!loading) {
//pass userEmail as a query parameter
fetchedTodos = await client.fetch(
`*[_type=="todo" && userEmail==$userEmail] | order(dueDate asc)
{_id, text, createdAt, dueDate, isCompleted, completedAt, userEmail}`,
{
userEmail: user.email,
});
//insert our response in the todoList state
setTodoList(fetchedTodos);
}
};
useEffect(
() => {
//now it will fetch todos on page load...
fetchTodos();
},
//this dependecy array tells React to run the
//hook again whenever the user loads or changes
[loading, user]
);
fetchTodos()
in the handleSubmit
function. Next polyfills the fetch
API for all browsers beforehand, so no need to worry about that!// src/pages/todos.js
// in handleSubmit function...
await fetch("/api/todo", {
method: "POST",
body: JSON.stringify({
text: userInput,
dueDate: dueDate,
user: user.email,
}),
});
//***uncomment this line now***
//after submitting, our TodoList will now refresh
await fetchTodos();
setUserInput("");
setErrMessage("");
setDueDate("");
}
};
//...
handleSubmit
, let's also add a handleDelete
function that we can pass to our <Todo/>
component. Since we're passing this function straight from the page to <Todo/>
(skipping the <TodoList/>
component), we should use React's useContext
hook. Context allows us to avoid passing props unnecessarily.fetchTodos()
to our context hook, so that we can get fresh data when we toggle a todo's status in its component.// src/pages/todos.js
import { useState, useEffect, createContext } from "react";
//... before the Page component
export const TodoContext = createContext()
export default function Todos() { //...
// then, below the handleSubmit function...
const handleDelete = async (selectedTodo) => {
await fetch("/api/todo", {
method: "DELETE",
body: selectedTodo._id,
});
//todos will refresh after delete, too
await fetchTodos();
};
console.log(todoList)
return (
<TodoContext.Provider value={{handleDelete, fetchTodos}>
{/* all your rendered JSX */}
</TodoContext.Provider>
src
. In it, create a new file called TodoList.js
. This will be a simple component that mainly exists to keep our todos.js
page a little cleaner.// src/components/TodoList.js
import Todo from "./Todo";
export default function TodoList({ todoList, user }) {
return (
<section>
<ul>
{/*if there are todos in the list...*/}
{todoList.length >= 1
? todoList.map((todo, idx) => {
//map only the user's todos
return user.email == todo.userEmail ? (
<Todo key={todo._id} todo={todo} />
) : (
""
);
})
: "Enter a todo item"}
</ul>
</section>
);
}
todoList
state in our page component to <TodoList/>
, which maps each item in the array to a <Todo/>
component. key
prop in the Todo because React requires it. React has more information about the key prop in their docs.Todo.js
. In the same folder, create that file. Remember the context we created in todos.js
? We can now put it into action.// src/components/Todo.js
import { useState, useContext } from "react";
// import a simple date formatting library
import dayjs from "dayjs";
// import a trashcan icon for our delete button
import { RiDeleteBin5Line } from "react-icons/ri";
import { TodoContext } from "../pages/todos"
export default function Todo({ todo }) {
//with useContext we do not need to pass extra props to <TodoList/>
const { handleDelete, fetchTodos } = useContext(TodoContext)
//setting states for the isCompleted boolean and a date completed
const [isCompleted, setIsCompleted] = useState(todo.isCompleted);
const [completedTime, setCompletedTime] = useState(todo.completedAt);
//function that syncs the completed checkbox with Sanity
const handleToggle = async (e) => {
e.preventDefault();
const result = await fetch("/api/todo", {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
id: todo._id,
//passes isCompleted React state to Sanity
isCompleted: isCompleted,
completedAt: todo.completedAt,
}),
});
const { status, completedAt } = await result.json();
// refresh our data
await fetchTodos();
//pass our Sanity results back into React
setIsCompleted(status);
setCompletedTime(completedAt);
};
return (
<li
className="bg-gray-50 my-6 border shadow-md rounded-xl
p-4 border-gray-200 flex justify-center items-center"
key={todo._id}
>
<input
className="mx-2 cursor-pointer"
type="checkbox"
checked={todo.isCompleted}
onChange={handleToggle}
/>
{/*if todo is done, cross it out and turn it gray*/}
<p
className={`text-lg mx-2 ${
todo.isCompleted ? "line-through text-gray-500" : ""
}`}
>
{todo.text}
</p>
<p className={`text-gray-400 mr-2`}>
{/*if todo is done, show completedTime
if not done, show due date */}
{todo.isCompleted
? `Done ${dayjs(completedTime).format("MMM D, YYYY")}`
: `Due ${dayjs(todo.dueDate).format("MMM D, YYYY")}`}
</p>
<button
className="mx-2"
onClick={(e) => {
e.preventDefault();
handleDelete(todo);
}}
>
<RiDeleteBin5Line />
</button>
</li>
);
}
// src/pages/todos.js
// ... at the bottom of the imports
import TodoList from "../components/TodoList"
// ... then directly under the form
<form> {/*...*/> </form>
<div className="my-12">
<h1 className="text-xl font-bold tracking-tight
my-8">Your Todos</h1>
{loading ? (
"loading..."
) : (
<TodoList
user={user}
todoList={todoList}
/>
)}
</div>
//...
npm run dev
and you should see your todo items appear. // src/pages/api/todo.js
//...
//after the POST request
case "PUT":
const result = await client
.patch(req.body.id)
.set({
isCompleted: !req.body.isCompleted,
//create new complete date if Todo is marked as done
completedAt: !!req.body.isCompleted ? "" : new Date().toISOString(),
})
.commit();
res.status(200).json({
status: result.isCompleted,
completedAt: result.completedAt,
});
break;
case "DELETE":
await client
.delete(req.body)
.then((res) => {
res.body;
})
.then((res) => console.log(`Todo was deleted`));
res.status(200).json({ msg: "Success" });
break;
//...
npm run dev
. You should now be able to mark todos complete and delete them.user.js
document in schemas
that allows each person to create their own profile. Then that document could be related to each todo of a given user via a reference field.