498 lines
13 KiB
Plaintext
498 lines
13 KiB
Plaintext
---
|
|
title: Build a Todo Manager with SvelteKit
|
|
description: Learn how to use Nhost with SvelteKit
|
|
sidebarTitle: SvelteKit
|
|
icon: S
|
|
---
|
|
|
|
In this tutorial, you will build a simple **Todo Manager** application with Nhost and React. Along the way you will interact with the Database, Authentication, and Storage services.
|
|
|
|
The Todo Manager will allow users to see public `todos` and sign in using a Magic Link to manage their own `todos` with attachments.
|
|
|
|
<CardGroup cols={3}>
|
|
<Card title="Database">
|
|
To store todos
|
|
</Card>
|
|
|
|
<Card title="Auth">
|
|
To sign in users
|
|
</Card>
|
|
|
|
<Card title="Storage">
|
|
To store attachments
|
|
</Card>
|
|
</CardGroup>
|
|
|
|
|
|
## Setup Nhost Backend
|
|
|
|
In this section, you will create and setup your first Nhost project.
|
|
|
|
### Create project
|
|
|
|
Create a new project in the [Nhost Dashboard](https://app.nhost.io).
|
|
|
|
Enter the details for your project and wait a couple of minutes while Nhost provisions your backend infrastructure:
|
|
|
|
- Dedicated PostgreSQL
|
|
- Realtime APIs over your data
|
|
- Authentication for managing your users
|
|
- Storage for handling files
|
|
|
|
### Create table `todos`
|
|
|
|
On the project's dashboard, navigate to **Database** and create a new table called `todos`.
|
|
|
|

|
|
|
|
You can either copy and paste the following SQL into the SQL Editor, **Database -> SQL Editor**, or manually create the table by clicking on **New Table**.
|
|
|
|
|
|
<Tabs>
|
|
<Tab title="SQL Editor">
|
|
Copy and paste the following SQL into the SQL Editor and press **Run**.
|
|
|
|
<Note>Please make sure to enable **Track this** so that the new table `todos` is available through the auto-generated APIs</Note>
|
|
|
|
```sql SQL
|
|
CREATE TABLE public.todos (
|
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
created_at timestamptz DEFAULT now() NOT NULL,
|
|
updated_at timestamptz DEFAULT now() NOT NULL,
|
|
title text NOT NULL,
|
|
completed bool DEFAULT 'false' NOT NULL,
|
|
file_id uuid,
|
|
user_id uuid NOT NULL,
|
|
PRIMARY KEY (id),
|
|
FOREIGN KEY (file_id) REFERENCES storage.files (id) ON UPDATE SET NULL ON DELETE SET NULL,
|
|
FOREIGN KEY (user_id) REFERENCES auth.users (id) ON UPDATE SET NULL ON DELETE SET NULL
|
|
);
|
|
```
|
|
</Tab>
|
|
<Tab title="UI">
|
|
Click on **New Table** and fill in the details for the `todos` table as shown.
|
|
|
|

|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
You should now see a new table called `todos` on the left panel, below **New Table**.
|
|
|
|
### Set permissions for todos
|
|
|
|
It's now time to set permission rules for the table you just created. With the table `todos` selected, click on **...**, followed by **Edit Permissions**.
|
|
|
|
You will set permissions for the `user` role and actions `insert`, `select`, `update`, and `delete`.
|
|
|
|
<Tabs>
|
|
<Tab title="insert">
|
|
Click on the right cell for the `user` role and action `insert` and set permissions as follows:
|
|

|
|
</Tab>
|
|
<Tab title="select">
|
|
Click on the right cell for the `user` role and action `select` and set permissions as follows:
|
|

|
|
</Tab>
|
|
<Tab title="update">
|
|
Click on the right cell for the `user` role and action `update` and set permissions as follows:
|
|

|
|
</Tab>
|
|
<Tab title="delete">
|
|
Click on the right cell for the `user` role and action `delete` and set permissions as follows:
|
|

|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### Set permissions for files
|
|
|
|
The `files` table is managed by Nhost and is defined on the `storage` schema. Click on the dropdown right next to `schema.public` and choose `schema.storage`.
|
|
|
|
With the `files` table selected, click on **...**, followed by **Edit Permissions**.
|
|
|
|
As before, we want to set permissions for the `user` role and actions `insert`, `select`, `delete`.
|
|
|
|
<Tabs>
|
|
<Tab title="insert">
|
|
Click on the right cell for the `user` role and action `insert` and set permissions as follows:
|
|

|
|
</Tab>
|
|
<Tab title="select">
|
|
Click on the right cell for the `user` role and action `select` and set permissions as follows:
|
|

|
|
</Tab>
|
|
<Tab title="delete">
|
|
Click on the right cell for the `user` role and action `delete` and set permissions as follows:
|
|

|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### Enable Sign In with Magic Link
|
|
|
|
To enable Magic Links, navigate to your project's **Settings -> Sign-In Methods**, toggle Magic Link, and save.
|
|
|
|
### Recap
|
|
|
|
<Steps>
|
|
<Step title="Nhost project created">
|
|
</Step>
|
|
|
|
<Step title="Database todos created">
|
|
</Step>
|
|
|
|
<Step title="Permissions set for todos and files">
|
|
</Step>
|
|
|
|
<Step title="Magic Link enabled">
|
|
</Step>
|
|
</Steps>
|
|
|
|
## Setup React Application
|
|
|
|
Now that we have Nhost configured, let's move on to setup the React application and the Nhost client.
|
|
|
|
### Create React Application
|
|
|
|
Run the following command in your terminal to create a React application using Vite.
|
|
|
|
```bash Terminal
|
|
npm create vite@latest nhost-react -- --template react
|
|
```
|
|
|
|
### Install Nhost React package
|
|
|
|
To install Nhost's React package, run the following command.
|
|
|
|
```bash Terminal
|
|
cd nhost-react && npm install @nhost/react
|
|
```
|
|
|
|
#### Configure the Nhost Client
|
|
|
|
Create a new file, `./src/lib/nhost.js`, with the following code to create a Nhost client. Replace `<SUBDOMAIN>` and `<REGION>` with the values from the project created earlier.
|
|
|
|
```ts ./src/lib/nhost.ts
|
|
import { NhostClient } from "@nhost/react";
|
|
|
|
export const nhost = new NhostClient({
|
|
subdomain: "<SUBDOMAIN>",
|
|
region: "<REGION>"
|
|
});
|
|
```
|
|
|
|
<Info>The project's `subdomain` and `region` can be found in the Nhost Dashboard under **Project Info**</Info>
|
|
|
|
### Setup Sign In Component
|
|
|
|
It is time to setup a new React component to handle the login functionality. Users will be able to sign in using a Magic Link.
|
|
|
|
Create a new file `./src/signin.jsx` with the following content:
|
|
|
|
```js ./src/signin.jsx
|
|
import { useState } from 'react'
|
|
import { useSignInEmailPasswordless } from '@nhost/react'
|
|
|
|
export default function SignIn() {
|
|
const [loading, setLoading] = useState(false)
|
|
const [email, setEmail] = useState('')
|
|
|
|
const { signInEmailPasswordless, error } = useSignInEmailPasswordless()
|
|
|
|
const handleSignIn = async (event) => {
|
|
event.preventDefault()
|
|
|
|
setLoading(true)
|
|
const { error } = await signInEmailPasswordless(email)
|
|
|
|
if (error) {
|
|
console.error({ error })
|
|
return
|
|
}
|
|
|
|
setLoading(false)
|
|
alert('Magic Link Sent!')
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1>Todo Manager</h1>
|
|
<p>powered by Nhost and React</p>
|
|
<form onSubmit={handleSignIn}>
|
|
<div>
|
|
<input
|
|
type="email"
|
|
placeholder="Your email"
|
|
value={email}
|
|
required={true}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<button disabled={loading}>
|
|
{loading ? <span>Loading</span> : <span>Send me a Magic Link!</span>}
|
|
</button>
|
|
</div>
|
|
{error && <p>{error.message}</p>}
|
|
</form>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Setup `Todos` Component
|
|
|
|
Now that users can sign in, let's move on and create the authenticated page that lists a user's todos and has a form for managing todos with attachments.
|
|
|
|
```js ./src/todos.jsx
|
|
import { useState, useEffect } from 'react'
|
|
import { useNhostClient, useFileUpload } from '@nhost/react'
|
|
|
|
const deleteTodo = `
|
|
mutation($id: uuid!) {
|
|
delete_todos_by_pk(id: $id) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const createTodo = `
|
|
mutation($title: String!, $file_id: uuid) {
|
|
insert_todos_one(object: {title: $title, file_id: $file_id}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const getTodos = `
|
|
query {
|
|
todos {
|
|
id
|
|
title
|
|
file_id
|
|
completed
|
|
}
|
|
}
|
|
`
|
|
|
|
export default function Todos() {
|
|
const [loading, setLoading] = useState(true)
|
|
const [todos, setTodos] = useState([])
|
|
|
|
const [todoTitle, setTodoTitle] = useState('')
|
|
const [todoAttachment, setTodoAttachment] = useState(null)
|
|
const [fetchAll, setFetchAll] = useState(false)
|
|
|
|
const nhostClient = useNhostClient()
|
|
const { upload } = useFileUpload()
|
|
|
|
useEffect(() => {
|
|
async function fetchTodos() {
|
|
setLoading(true)
|
|
const { data, error } = await nhostClient.graphql.request(getTodos)
|
|
|
|
if (error) {
|
|
console.error({ error })
|
|
return
|
|
}
|
|
|
|
setTodos(data.todos)
|
|
setLoading(false)
|
|
}
|
|
|
|
fetchTodos()
|
|
|
|
return () => {
|
|
setFetchAll(false)
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [fetchAll])
|
|
|
|
const handleCreateTodo = async (e) => {
|
|
e.preventDefault()
|
|
|
|
let todo = { title: todoTitle }
|
|
if (todoAttachment) {
|
|
const { id, error } = await upload({
|
|
file: todoAttachment,
|
|
name: todoAttachment.name
|
|
})
|
|
|
|
if (error) {
|
|
console.error({ error })
|
|
return
|
|
}
|
|
|
|
todo.file_id = id
|
|
}
|
|
|
|
const { error } = await nhostClient.graphql.request(createTodo, todo)
|
|
|
|
if (error) {
|
|
console.error({ error })
|
|
}
|
|
|
|
setTodoTitle('')
|
|
setTodoAttachment(null)
|
|
setFetchAll(true)
|
|
}
|
|
|
|
const handleDeleteTodo = async (id) => {
|
|
if (!window.confirm('Are you sure you want to delete this TODO?')) {
|
|
return
|
|
}
|
|
|
|
const todo = todos.find((todo) => todo.id === id)
|
|
if (todo.file_id) {
|
|
await nhostClient.storage.delete({ fileId: todo.file_id })
|
|
}
|
|
|
|
const { error } = await nhostClient.graphql.request(deleteTodo, { id })
|
|
if (error) {
|
|
console.error({ error })
|
|
}
|
|
|
|
setFetchAll(true)
|
|
}
|
|
|
|
const completeTodo = async (id) => {
|
|
const { error } = await nhostClient.graphql.request(
|
|
`
|
|
mutation($id: uuid!) {
|
|
update_todos_by_pk(pk_columns: {id: $id}, _set: {completed: true}) {
|
|
completed
|
|
}
|
|
}
|
|
`,
|
|
{ id }
|
|
)
|
|
|
|
if (error) {
|
|
console.error({ error })
|
|
}
|
|
|
|
setFetchAll(true)
|
|
}
|
|
|
|
const openAttachment = async (todo) => {
|
|
const { presignedUrl, error } = await nhostClient.storage.getPresignedUrl({
|
|
fileId: todo.file_id
|
|
})
|
|
|
|
if (error) {
|
|
console.error({ error })
|
|
return
|
|
}
|
|
|
|
window.open(presignedUrl.url, '_blank')
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="container">
|
|
<div className="form-section">
|
|
<h2>Add a new TODO</h2>
|
|
<form onSubmit={handleCreateTodo}>
|
|
<div className="input-group">
|
|
<label htmlFor="title">Title</label>
|
|
<input
|
|
id="title"
|
|
type="text"
|
|
placeholder="Title"
|
|
value={todoTitle}
|
|
onChange={(e) => setTodoTitle(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="input-group">
|
|
<label htmlFor="file">File (optional)</label>
|
|
<input id="file" type="file" onChange={(e) => setTodoAttachment(e.target.files[0])} />
|
|
</div>
|
|
<div className="submit-group">
|
|
<button type="submit" disabled={!todoTitle}>
|
|
Add Todo
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div className="todos-section">
|
|
{(!loading &&
|
|
todos.map((todo) => (
|
|
<div className="todo-item" key={todo.id ?? 0}>
|
|
<input
|
|
type="checkbox"
|
|
checked={todo.completed}
|
|
disabled={todo.completed}
|
|
id={`todo-${todo.id}`}
|
|
onChange={() => completeTodo(todo.id)}
|
|
/>
|
|
{todo.file_id && (
|
|
<span>
|
|
<a onClick={() => openAttachment(todo)}> Open Attachment</a>
|
|
</span>
|
|
)}
|
|
<label htmlFor={`todo-${todo.id}`} className="todo-title">
|
|
{todo.completed && <s>{todo.title}</s>}
|
|
{!todo.completed && todo.title}
|
|
</label>
|
|
<button type="button" onClick={() => handleDeleteTodo(todo.id)}>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
))) || (
|
|
<div className="todo-item">
|
|
<label className="todo-title">Loading...</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="sign-out-section">
|
|
<button type="button" onClick={() => nhostClient.auth.signOut()}>
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
With both `SignIn` and `Todos` in place, update `./src/App.jsx` to use the new components:
|
|
|
|
```js ./src/App.jsx
|
|
import './App.css'
|
|
import { NhostProvider } from '@nhost/react'
|
|
import { nhost } from './lib/nhost.js'
|
|
import SignIn from './signin'
|
|
import Todos from './todos'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
function App() {
|
|
const [session, setSession] = useState(null)
|
|
|
|
useEffect(() => {
|
|
setSession(nhost.auth.getSession())
|
|
|
|
nhost.auth.onAuthStateChanged((_, session) => {
|
|
setSession(session)
|
|
})
|
|
}, [])
|
|
|
|
return (
|
|
<NhostProvider nhost={nhost}>
|
|
{session ? <Todos session={session} /> : <SignIn />}
|
|
</NhostProvider>
|
|
)
|
|
}
|
|
|
|
export default App
|
|
```
|
|
|
|
|
|
## The End
|
|
|
|
Run the Todo Manager with:
|
|
|
|
```bash Terminal
|
|
npm run dev -- --open --port 3000
|
|
```
|
|
|
|
Open your browser on [localhost:3000](localhost:3000) to see your new application in action.
|
|
|