Files
nhost/docs/getting-started/tutorials/sveltekit.mdx
2025-09-24 14:50:55 +02:00

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`.
![Database](/images/tutorials/todos-react-database.png)
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.
![New Table](/images/tutorials/todos-react-database-new-table.png)
</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:
![User Insert](/images/tutorials/todos-react-permissions-insert.png)
</Tab>
<Tab title="select">
Click on the right cell for the `user` role and action `select` and set permissions as follows:
![User Select](/images/tutorials/todos-react-permissions-select.png)
</Tab>
<Tab title="update">
Click on the right cell for the `user` role and action `update` and set permissions as follows:
![User Select](/images/tutorials/todos-react-permissions-update.png)
</Tab>
<Tab title="delete">
Click on the right cell for the `user` role and action `delete` and set permissions as follows:
![User Delete](/images/tutorials/todos-react-permissions-delete.png)
</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:
![User Insert](/images/tutorials/todos-react-permissions-files-insert.png)
</Tab>
<Tab title="select">
Click on the right cell for the `user` role and action `select` and set permissions as follows:
![User Select](/images/tutorials/todos-react-permissions-files-select.png)
</Tab>
<Tab title="delete">
Click on the right cell for the `user` role and action `delete` and set permissions as follows:
![User Delete](/images/tutorials/todos-react-permissions-files-delete.png)
</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.