Add nuxt tutorial

This commit is contained in:
Evelina Berg
2023-10-25 23:10:05 +02:00
parent 8fe67b887f
commit 6bae4383bb
11 changed files with 765 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import { globToTutorial } from '$lib/utils/tutorials.js';
import { setContext } from 'svelte';
export let data;
const tutorials = globToTutorial(data);
setContext('tutorials', tutorials);
</script>
<slot />

View File

@@ -0,0 +1,11 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = ({ url }) => {
const tutorials = import.meta.glob('./**/*.markdoc', {
eager: true
});
return {
tutorials,
pathname: url.pathname
};
};

View File

@@ -0,0 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
throw redirect(303, '/docs/tutorials/nuxt/step-1');
};

View File

@@ -0,0 +1,35 @@
---
layout: tutorial
title: Build an ideas tracker with Nuxt
description: Learn to build an idea tracker app with Appwrite and Nuxt with authentication, databases and collections, queries, pagination, and file storage.
step: 1
difficulty: beginner
readtime: 12
back: /docs
---
**Idea tracker**: an app to track all the side project ideas that you'll start, but probably never finish.
In this tutorial, you will build Idea tracker with Appwrite and Nuxt.
{% only_dark %}
![Create project screen](/images/docs/tutorials/dark/idea-tracker.png)
{% /only_dark %}
{% only_light %}
![Create project screen](/images/docs/tutorials/idea-tracker.png)
{% /only_light %}
# Concepts {% #concepts %}
This tutorial will introduce the following concepts:
1. Setting up your first project
2. Authentication
3. Navigation
4. Databases and collections
5. Queries
# Prerequisites {% #prerequisites %}
1. Basic knowledge of JavaScript.
2. Have [Node.js](https://nodejs.org/en) and [NPM](https://www.npmjs.com/) installed on your computer.

View File

@@ -0,0 +1,98 @@
---
layout: tutorial
title: Create app
description: Create a Nuxt app project and integrate with Appwrite
step: 2
---
# Create Nuxt project {% #create-nuxt-project %}
Create a Nuxt app with the `npx init` command.
```sh
npx nuxi@latest init ideas-tracker
```
# Add dependencies {% #add-dependencies %}
Install the JavaScript Appwrite SDK.
```sh
npm install appwrite
```
You can start the development server to watch your app update in the browser as you make changes.
```sh
npm run dev
```
# Set up files {% #set up-files %}
In Nuxt, directories help organize the codebase and minimize boilerplate.
The purpose is making it easy to find and manage different aspect of your application.
The files added to the `pages` directory will automatically become a route.
Nuxt will look for a layout in the `layouts` directory to render a page layout.
Without a layout file in this directory, the routing won't work
When these files have been created the `app.vue` file needs to edited to render these pages instead of the standard welcome page.
Then, we will have a working app where we can verify our changes in the browser throughout the tutorial.
As an additional step, we will be adding [Appwrite's Pink Design system]('https://pink.appwrite.io/') to have a global design system to help with the layout.
First, add a page by creating the file `src/pages/index.vue` and add the following code.
```vue
<template>
<div>
<h1>Hello, idea tracker!</h1>
</div>
</template>
```
Create the file `src/layouts/default.vue` to add the default layout.
```vue
<template>
<div>
<slot />
</div>
</template>
<script>
export default {
layout: "default",
};
</script>
```
Go to `app.vue`, remove `NuxtWelcome`and insert `<NuxtPage />` wrapped inside `<NuxtLayout>`.
```vue
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
```
To import Appwrite's design system to all pages and components, edit the file `nuxt.config.ts`.
The layouts is then ready to be used in the templates with auto-import.
```ts
export default defineNuxtConfig({
app: {
head: {
link: [
{ rel: "stylesheet", href: "https://unpkg.com/@appwrite.io/pink" },
{
rel: "stylesheet",
href: "https://unpkg.com/@appwrite.io/pink-icons",
},
],
},
},
devtools: { enabled: true },
});
```

View File

@@ -0,0 +1,59 @@
---
layout: tutorial
title: Set up Appwrite
description: Import and configure a project with Appwrite Cloud.
step: 3
---
# Create project {% #create-project %}
Head to the [Appwrite Console](https://cloud.appwrite.io/console).
{% only_dark %}
![Create project screen](/images/docs/quick-starts/dark/create-project.png)
{% /only_dark %}
{% only_light %}
![Create project screen](/images/docs/quick-starts/create-project.png)
{% /only_light %}
If this is your first time using Appwrite, create an account and create your first project.
Then, under **Add a platform**, add a **Web app**.
The **Hostname** should be localhost.
{% only_dark %}
![Add a platform](/images/docs/quick-starts/dark/add-platform.png)
{% /only_dark %}
{% only_light %}
![Add a platform](/images/docs/quick-starts/add-platform.png)
{% /only_light %}
You can skip the optional steps.
# Initialize Appwrite SDK {% #init-sdk %}
To use Appwrite in our app, we'll need to find our project ID.
It is located in the **Settings** page.
{% only_dark %}
![Project settings screen](/images/docs/quick-starts/dark/project-id.png)
{% /only_dark %}
{% only_light %}
![Project settings screen](/images/docs/quick-starts/project-id.png)
{% /only_light %}
Create a new file `src/appwrite.js` for the Appwrite related code.
Only one instance of the `Client()` class should be created per app.
Add the following code to it, replacing `<YOUR_PROJECT_ID>` with your project ID.
```js
import { Client, Databases, Account } from "appwrite";
const client = new Client();
client
.setEndpoint("https://cloud.appwrite.io/v1")
.setProject("<YOUR_PROJECT_ID>"); // Replace with your project ID
export const account = new Account(client);
export const database = new Databases(client);
```

View File

@@ -0,0 +1,196 @@
---
layout: tutorial
title: Add authentication
description: Add authentication to your Nuxt application using Appwrite Web SDK
step: 4
---
# Composables {% #composables %}
Composables is a design pattern in Vue and Nuxt to manage logic related to data fetching and global state management.
You can think of a composable as a way to put pieces of code that do specific tasks into neat little boxes that you can use over and over again in you app.
In the user session composable will put all the logic related to user authentication, such as login, logout and authentication status.
It will be available for access in all pages and components.
Create your composable for the user session logic by adding the file `src/composables/useUserSession.js`.
Add the following code.
```js
import { ID } from 'appwrite'
import { ref } from 'vue'
import { account } from '/appwrite'
const current = ref(null) //Reference to the current user object
const isLoggedIn = ref(null) //Reference to check user status
export const useUserSession = () => {
const register = async (email, password) => {
await account.create(ID.unique(), email, password) //Interaction with the Appwrite backend
await this.login(email, password) //Login the newly created user
}
const login = async (email, password) => {
const authUser = await account.createEmailSession(email, password) //Interaction with the Appwrite backend
current.value = authUser //Passing user data to the current user reference
isLoggedIn.value = true //Setting user status to true
}
const logout = async () => {
await account.deleteSession('current') //Interaction with the Appwrite backend
current.value = null //Clearing the current user data object
isLoggedIn.value = false //Setting user status to false
}
return {
current,
isLoggedIn,
login,
logout,
register,
}
```
The `useUserSession()` function is neatly packed and ready to be reused in our pages and components.
# Login page {% #login-page %}
With the authentication functions handled, we can go on to build a simple login page.
The user interactions will be handled in a form with two input fields for email and password.
Underneath will be a button to login and a button to register a new user.
The values from the inputs will be bound to a `v-model`.
The `v-model`acts like a bridge between your input fields and your Javascript code.
If you change the information with yout code, the input field also changes.
Similarily, if you type something in the input field, the information in your code also updates.
It is a two-way-cnnection that are magically links them together.
Create the form by adding a new file `src/pages/index.vue` and put in the following code.
```vue
<template>
<div class="card u-margin-32">
<h2 class="eyebrow-heading-2">Login/Register</h2>
<form
class="form u-width-full-line u-max-width-500 u-margin-block-start-16"
@submit.prevent="handleLogin || handleRegistration"
>
<ul class="form-list">
<li class="form-item">
<label class="label">Email</label>
<div class="input-text-wrapper">
<!-- Input with v-model to link email field to script -->
<input
v-model="userData.email"
type="email"
class="input-text"
placeholder="Email"
required
/>
</div>
</li>
<li class="form-item">
<label class="label">Password</label>
<div class="input-text-wrapper">
<!-- Input with v-model to link password field to script -->
<input
v-model="userData.password"
type="password"
class="input-text"
placeholder="Password"
required
/>
</div>
</li>
</ul>
<ul class="buttons-list u-margin-block-start-16">
<li class="buttons-list-item">
<!-- Login button -->
<button
class="button is-small u-margin-inline-start-4"
aria-label="Login"
@click="handleLogin"
>
Login
</button>
</li>
<li class="buttons-list-item">
<!-- Register button -->
<button
class="button is-small is-secondary u-margin-inline-start-4"
aria-label="Register account"
@click="handleRegistration"
>
Register
</button>
</li>
</ul>
</form>
</div>
</template>
<script>
export default {
setup() {
//Accessing the user session composable
const user = useUserSession();
//V-model object
const userData = {
email: "",
password: "",
};
//Event handler for logging in
const handleLogin = async () => {
await user.login(userData.email, userData.password);
};
//Event handler for registrering
const handleRegistration = async () => {
await user.register(userData.email, userData.password);
};
return {
handleLogin,
handleRegistration,
userData,
};
},
};
</script>
```
# Home page {% #home-page %}
Now that users can register and login to the app, let's modify the home page in `src/pages/index.vue` to give the logged in users a secret message.
```vue
<template>
<div>
<!-- Visible if user is logged in -->
<section v-if="user.isLoggedIn.value === true">
<h2>Welcome!</h2>
</section>
<section>
<h2>Hello, idea tracker!</h2>
</section>
</div>
</template>
<script>
export default {
setup() {
//Accessing the user session composable
const user = useUserSession();
return {
user,
};
},
};
</script>
```

View File

@@ -0,0 +1,58 @@
---
layout: tutorial
title: Add navigation
description: Add navigation to your app.
step: 5
---
To help our users navigate the app we want it to have a navigation bar that's visible on all pages.
We will use the `useUserSession` composable to show a login button when no user is logged in and a logout button when logged in.
Open the `app.vue` file and add the navbar in the `template`, just above the `<NuxtPage />` tag.
```vue
...
<NuxtLayout>
<!--- Navbar -->
<nav class="main-header u-padding-inline-end-0">
<h3 class="u-stretch eyebrow-heading-1">Idea Tracker</h3>
<!-- Email and logout button if logged in user -->
<div
class="main-header-end u-margin-inline-end-16"
v-if="user.isLoggedIn.value === true"
>
<p>
{{ user.current.value.providerUid }}
</p>
<button class="button" type="button" @click="user.logout()">
Logout
</button>
</div>
<!-- Login button if no user logged in -->
<NuxtLink v-else href="/login" class="button u-margin-inline-end-16">
Login
</NuxtLink>
</nav>
<NuxtPage />
</NuxtLayout>
...
```
Add a new script below the template to get information about the user session from the `useUserSession`composable.
```vue
<script>
export default {
setup() {
const user = useUserSession(); //Access the user session composable
return {
user,
};
},
};
</script>
```
You might wonder about the v-

View File

@@ -0,0 +1,79 @@
---
layout: tutorial
title: Add database
description: Add databases and queries for ideas in your Nuxt project.
step: 6
---
In Appwrite, data is stored as a collection of documents. Create a collection in the [Appwrite Console](https://cloud.appwrite.io/) to store our ideas.
{% only_dark %}
![Create project screen](/images/docs/tutorials/dark/idea-tracker-collection.png)
{% /only_dark %}
{% only_light %}
![Create project screen](/images/docs/tutorials/idea-tracker-collection.png)
{% /only_light %}
Create a new collection with the following attributes:
| Field | Type | Required |
|-------------|--------|----------|
| userId | String | Yes |
| title | String | Yes |
| description | String | No |
Change the collection's permissions in the settings to give access.
First, add an "Any" role and check the `READ` box.
Next, add a "Users" role and give them access to create, update and delete.
# Query methods {% #query-methods %}
Now that you have a collection to hold ideas, we can use it to get, add and remove data from our app.
We will create a new composable to manage the data fetching and their global state.
Create a new file in the composables directory, `src/composables/useIdeas.js` and add the following code.
```js
import { ID, Query } from "appwrite";
import { database } from "~/appwrite";
import { ref } from "vue";
const IDEAS_DATABASE_ID = "YOUR_DATABASE_ID";
const IDEAS_COLLECTION_ID = "YOUR_COLLECTION_ID";
const current = ref(null); //Reference for the fetched data
export const useIdeas = () => {
/* Fetch the 10 most recent ideas from the database
and passes the list to the current reference object */
const init = async () => {
const response = await database.listDocuments(
IDEAS_DATABASE_ID,
IDEAS_COLLECTION_ID,
[Query.orderDesc("$createdAt"), Query.limit(10)]
);
current.value = response.documents;
};
/* Add new idea to the database, add it to the current
object and remove the last to still have 10 ideas */
const add = async (idea) => {
const response = await database.createDocument(
IDEAS_DATABASE_ID,
IDEAS_COLLECTION_ID,
ID.unique(),
idea
);
current.value = [response, ...current.value].slice(0, 10);
const remove = async (id) => {
await database.deleteDocument(IDEAS_DATABASE_ID, IDEAS_COLLECTION_ID, id);
await init(); //Refetch ideas to ensure we have 10 items
};
return {
add,
current,
init,
remove,
};
};
```

View File

@@ -0,0 +1,203 @@
---
layout: tutorial
title: Create ideas page
description: Add database queries using Appwrite in your Nuxt app.
step: 7
---
# Create ideas page {% #create-ideas-page %}
Using the `useIdeas` composable, we can now get some ideas on the page to read and interact with.
We don't have any to show just yet, so we are going to start with adding a form to create new ones.
The form will only be visible for logged in users.
We begin with adding the `v-model` object in the `<script>` tag on `src/pages/index.vue`.
Unlike the `v-model`on the login form, the properties for the input fields will have `ref` to make them even more reactive to changes.
The difference is that we want to instantly clear the inputs when the user press the submit button.
On the login page, we redirected the user to the home page, so this was not necessary.
Defining our object, we also need to think more closely about what the database is expecting to get.
Not only does it want the title and the description, first it expects a userId.
We can't expect our user to enter that, so we will add the one we have in the user session composable.
This value does not require a `ref` since it is not a part of any other interactions.
```vue
<script>
export default {
setup() {
const ideas = useIdeas(); // New variable for the ideas composable
const user = useUserSession();
// V-model object
const addIdeaData = {
title: ref(""),
description: ref(""),
};
// Event handler to submit form
const handleAddIdea = async () => {
// Extract the values from the refs and add userId
const postIdeaData = {
userId: user.current.value.userId,
title: addIdeaData.title.value,
description: addIdeaData.title.value
}
await ideas.add(postIdeaData);
addIdeaData.title.value = ""; // Clears the title field
addIdeaData.description.value = ""; // Clears the desciption field
};
return {
addIdeaData,
handleAddIdea,
user,
};
},
};
</script>
```
Let's remove the header with the secret message we added in step 4 and replace it with a form.
Since the properties for the `v-model` variables are references, we use the `.value` property to get the correct information to show.
```vue
...
<!-- Section to display for logged in users -->
<section v-if="user.isLoggedIn.value === true" class="u-margin-32">
<article class="box">
<h4 class="heading-level-4">Submit Idea</h4>
<!-- Form -->
<form @submit.prevent="handleAddIdea" class="u-margin-block-start-16">
<ul class="form-list">
<li class="form-item">
<!-- Input for title -->
<label class="label">Title</label>
<input
v-model="addIdeaData.title.value"
type="text"
placeholder="Title"
/>
</li>
<li class="form-item">
<!-- Input for description -->
<label class="label">Description</label>
<textarea
v-model="addIdeaData.description.value"
placeholder="Description"
/>
</li>
<button class="button" aria-label="Submit idea" type="submit">
Submit
</button>
</ul>
</form>
</article>
</section>
<!-- Section to display if the user is not logged in -->
<section v-else><p>Please login to submit an idea.</p></section>
...
```
The next step is to get the newly added ideas from the database and show them below the previous sections.
This section is visible to all users.
Since we need the data before any interaction has taken place on the page, like pressing a button, we have to use a function to catch the moment when the page loads.
The built-in `onMounted` function does just that.
Inside it we can call the `init()` function from the ideas composable to get the data.
Add the following to the `setup()`in the `<script>`, just underneath the `useIdeas` variable.
```vue
setup() {
<!-- After composable variables -->
onMounted(() => {
ideas.init();
});
...
}
```
Then continue with adding a new section in the template that displays the list of ideas.
```vue
<!-- Form section -->
...
<!-- New ideas section -->
<section class="u-margin-32">
<article class="card">
<h4 class="heading-level-4">Latest Ideas</h4>
<ul>
<li v-for="idea in ideas.current.value">
<div class="box">
<h5 class="heading-level-6">{{ idea.title }}</h5>
<p class="body-text-2">{{ idea.description }}</p>
</div>
</li>
</ul>
</article>
</section>
...
```
Great, we can enter new ideas and have look at them!
It probably won't be long before a user will want to delete an idea.
We help the by adding a delete button in the top, right corner of their ideas.
First, we'll add a function to handle the event in the `setup()`.
```vue
setup() {
...
// New eventhandler
const handleRemoveIdea = async (id) => {
await ideas.remove(id);
};
return {
...
handleRemoveIdea, // Return the added event handler
...
};
};
```
The button is added to the `div` that renders the details of an idea.
```vue
...
<div class="box">
<h5 class="heading-level-6">{{ idea.title }}</h5>
<p class="body-text-2">{{ idea.description }}</p>
<!-- New div with icon button -->
<div
class="u-position-absolute u-inset-inline-end-8 u-inset-block-start-8"
>
<button
class="button is-small is-text is-only-icon"
aria-label="Remove item"
<!-- Ensuring button is only displayed if a user is logged in and the id's are a match -->
v-if="
user.current.value &&
idea.userId === user.current.value.userId
"
type="button"
@click="handleRemoveIdea(idea.$id)"
>
<span class="icon-document-remove" aria-hidden="true" />
</button>
</div>
</div>
...
```
Just one last thing before it's all wrapped up -- the layout needs a finishing touch.
At the bottom of the file, under the closing `</script>` tag, add a style with a class to change the background color of the list of cards with a color variable.
```vue
<style>
article.box {
background-color: hsl(var(--color-neutral-0));
}
</style>
```

View File

@@ -0,0 +1,10 @@
---
layout: tutorial
title: Next steps
description: View your Nuxt app built on Appwrite Cloud.
step: 8
---
# Test your project {% #test-project %}
Run your project with `npm run dev -- --open --port 3000` and open [http://localhost:3000](http://localhost:3000) in your browser.
Head to the [Appwrite Console](https://cloud.appwrite.io/console) to see the new users and follow their interactions.