Merge branch 'main' into feat-nuxt-tutorial

This commit is contained in:
Vincent (Wen Yu) Ge
2024-01-18 14:57:58 +00:00
92 changed files with 2152 additions and 1207 deletions

View File

@@ -1,10 +0,0 @@
<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

@@ -1,11 +0,0 @@
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

@@ -1,6 +0,0 @@
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

@@ -1,161 +0,0 @@
---
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.
The command will install all the necessary dependencies for you.
```sh
npx nuxi@latest init ideas-tracker
```
# Add dependencies {% #add-dependencies %}
Once the project is created, change your current working directory and install the JavaScript Appwrite SDK.
```sh
cd ideas-tracker
npm install appwrite
npm install "@appwrite.io/pink"
```
Open `App.vue` and import the relevant style files.
```html
<!-- app.vue -->
<script setup>
import "@appwrite.io/pink";
// optionally, add icons
import "@appwrite.io/pink-icons";
</script>
```
Then update `nuxt.config.ts` to disable SSR for now. SSR support is coming soon to Appwrite, for now, disable SSR.
```ts
// nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
ssr: false,
devtools: { enabled: true }
})
```
You can start the development server to watch your app update in the browser as you make your changes.
```sh
npm run dev
```
# File structure {% #file-structure %}
Nuxt relies on an opiniated directory structure to automate tasks and help organize the codebase.
To take advantage of this we need to add the following directories:
- `/components/` to keep our UI components in one place.
We will get back to it in [step 5](/docs/tutorials/nuxt/step-5)
- `/composables/`for storing files handling global states and data fetching.
We will use it in [step 4](/docs/tutorials/nuxt/step-4)
- `/layouts/` to store the page layouts
- `/pages/` for the content pages.
Run the following command to create these folders
```sh
mkdir components composables layouts pages
```
# Add layout {% #add-layout %}
Go to the `layouts/` directory and add the file `default.vue`.
Add the following code for the default layout.
As you see it's nearly empty but it is needed for the automatic routing to work properly.
```html
<!-- layouts/default.vue -->
<template>
<div>
<slot />
</div>
</template>
<script>
export default {
layout: "default",
};
</script>
```
# Add home page {% #add-home-page %}
Next, head over to the `pages`directory.
This is where we will keep the content that will render on our pages in the web application.
Each file you put in here will automatically become a route.
Add the file `index.vue` with the following code.
```vue
<!-- pages/index.vue -->
<template>
<nav class="main-header u-padding-inline-end-0">
<h3 class="u-stretch eyebrow-heading-1">Hello, Idea Tracker!</h3>
</nav>
</template>
```
This is what your directory should look like after adding the new directories and files:
```
[repository tree]
├── .nuxt/
├── components/
├── composables/
├── layouts/
│ └── default.vue
├── pages/
│ ├── index.vue
├── public/
│ └── /favicon.ico
├── server/
│ └── /tsconfig.json
├── .gitignore
├── app.vue
├── nuxt.config.ts
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```
# Render page {% #render-page %}
If you run the development server now, it will still render the Nuxt Welcome page.
We need to tell our app to use the files we just created instead.
Open `app.vue` in the root directory and replace the content with the following code.
Your page will now be up and running.
```vue
<!-- app.vue -->
<script setup>
import "@appwrite.io/pink";
import "@appwrite.io/pink-icons";
</script>
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
```

View File

@@ -1,73 +0,0 @@
---
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.
# Environment variables {% #environment-variables %}
To connect to Appwrite in our app, we'll need to use sensitive information.
We keep the secrets by using environment variables for the endpoint and project id.
Your project id is located in the **Settings** page in the Appwrite console.
{% 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 %}
Add a `.env` file to the root directory and add the following code to it, replacing `YOUR_PROJECT_ID` with your project id.
```
VITE_APPWRITE_ENDPOINT="https://cloud.appwrite.io/v1"
VITE_APPWRITE_PROJECT="YOUR_PROJECT_ID"
```
# Initialize Appwrite SDK {% #init-sdk %}
Create a new file `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.
```ts
// appwrite.ts
import { Client, Databases, Account } from "appwrite";
const url: string = import.meta.env.VITE_APPWRITE_ENDPOINT;
const project: string = import.meta.env.VITE_APPWRITE_PROJECT;
const client: Client = new Client();
client.setEndpoint(url).setProject(project);
export const account: Account = new Account(client);
export const database: Databases = new Databases(client);
```

View File

@@ -1,199 +0,0 @@
---
layout: tutorial
title: Add authentication
description: Add authentication to your Nuxt application using Appwrite Web SDK
step: 4
---
For our ideas tracker app, we want any visitor to be able to read the ideas that are stored.
On the other hand, we don't want the page spammed with just about anything from anyone just stopping by.
To prevent that, or at least making it a bit more difficult, editing ideas will be available for logged in users only.
With a login function, we can differentiate between users and decide which users have access to which content.
We will build a page with a simple login form and store its related logic in a `useUserSession` composable so it can be reused, starting with the composable.
# User session composable {% #user-session-composable %}
There are a few standard functions involved in handling a user session that is added to the composable.
The user needs to be able to register to an account, login to the account and logout from it.
We are using Appwrite as a backend to handle the user details, so we need to connect to Appwrite by importing the configurations from step 3.
The response from these interactions will be stored as references to get more information about the user in our app.
In your `composable` directory, create the file `useUserSession.js` and add the following code.
Then you can call the `useUserSession()` function in the pages and components to use the functionality.
```ts
// composable/useUserSession.ts
import { ID } from "appwrite";
import { ref } from "vue";
import { account } from "../appwrite";
import { type Models } from 'appwrite';
const current = ref<Models.Session | null>(null); // Reference to current user object
export const useUserSession = () => {
const register = async (email: string, password: string): Promise<void> => {
await account.create(ID.unique(), email, password); // Register new user in Appwrite
await login(email, password); // Login registered user
};
const login = async (email: string, password: string): Promise<void> => {
const authUser = await account.createEmailSession(email, password); // Open user session in Appwrite
current.value = authUser; // Pass user data to current ref
navigateTo("/");
};
const logout = async (): Promise<void> => {
await account.deleteSession("current"); // Delete Appwrite user session
current.value = null; // Clear current ref
navigateTo("/");
};
// Check if already logged in to initialize the store.
account.getSession('current').then((user: Models.Session) => {
current.value = user;
});
return {
current,
login,
logout,
register,
};
};
```
# Login page {% #login-page %}
Create a new file in the `pages` directory called `login.vue`.
This will not only create a new page, it will also add the route `/login` to the url.
In step 5 we will add a login button that will redirect us to this page.
We will define functions to handle form submissions and show either a signup
or a login form, which renders one form or the other depending on `isSignUp`'s state.
We will also show buttons to toggle between the two different types of forms.
```vue
<!-- pages/login.vue -->
<script setup>
import { ref } from 'vue';
// Access user composable functions
const user = useUserSession();
const isSignUp = ref(false);
// Login user event handler
const handleLogin = async (event) => {
const form = event.target;
const formData = new FormData(form);
await user.login(formData.get('email'), formData.get('password'));
form.reset(); // Clear the form
};
const handleRegistration = async (event) => {
const form = event.target;
const formData = new FormData(form);
await user.register(formData.get('email'), formData.get('password'));
form.reset(); // Clear the form
};
</script>
<template>
<div class="u-max-width-650" style="margin: 0 auto;">
<section class="card u-margin-32">
<h2 class="eyebrow-heading-2">Login/Register</h2>
<AuthForm v-if="isSignUp" :handle-submit="handleRegistration" submit-type="Sign Up"></AuthForm>
<AuthForm v-else :handle-submit="handleLogin" submit-type="Log In"></AuthForm>
<button v-if="isSignUp" @click="isSignUp = false" class="u-margin-block-start-16">
Already have an account? Log in
</button>
<button v-else @click="isSignUp = true" class="u-margin-block-start-16">
Don't have an account? Sign up
</button>
</section>
</div>
</template>
```
# Authentication forms {% #auth-forms %}
In the previous step, we defined a `AuthForm` to handle signup and login.
Let's build this form now.
Create a new file `components/authForm.vue` and add the following code.
The handle submit and submit type are props passed from `pages/login.vue`
```html
<!-- components/authForm.vue -->
<template>
<form
class="form u-width-full-line u-max-width-500 u-margin-block-start-16"
@submit.prevent="handleSubmit"
>
<ul class="form-list">
<!-- Input field e-mail -->
<li class="form-item">
<label class="label">Email</label>
<div class="input-text-wrapper">
<input
type="email"
name="email"
class="input-text"
placeholder="Email"
required
/>
</div>
</li>
<!-- Input field e-mail -->
<li class="form-item">
<label class="label">Password</label>
<div class="input-text-wrapper">
<input
type="password"
name="password"
class="input-text"
placeholder="Password"
required
/>
</div>
</li>
</ul>
<ul class="buttons-list u-margin-block-start-16">
<!-- Login button -->
<li class="buttons-list-item">
<button
class="button is-small u-margin-inline-start-4"
aria-label="Login"
@click="handleSubmit"
>
{{submitType}}
</button>
</li>
</ul>
</form>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
handleSubmit: {
type: Function,
required: true
},
submitType: {
type: String,
required: true
}
})
</script>
```
Go to the browser and add `/login` to the url to check out the new page.

View File

@@ -1,68 +0,0 @@
---
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 once again use the `useUserSession()` composable for information about the current user.
With this piece of information we will show a login button when no user is logged in and a logout button when one is.
We will also put the user's e-mail address next to the logout button.
From the `components` directory, create the file `navbar.vue` and add the code below.
```vue
<!-- components/navbar.vue -->
<script setup>
// Access user composable function
const user = useUserSession();
</script>
<template>
<div>
<!--- 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.current.value"
>
<p>{{ user.current.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>
</div>
</template>
```
Open `app.vue` from the root directory and add the navigation bar.
```vue
<!-- app.vue -->
<script setup>
import "@appwrite.io/pink";
import "@appwrite.io/pink-icons";
</script>
<template>
<div>
<NuxtLayout>
<!-- Add navbar -->
<Navbar />
<NuxtPage />
</NuxtLayout>
</div>
</template>
```
Have a look in the browser at both the main page and the login page to test the new functionality.

View File

@@ -1,115 +0,0 @@
---
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 new database and collection in the [Appwrite Console](https://cloud.appwrite.io/) to store the ideas.
{% only_dark %}
![Create collection screen](/images/docs/tutorials/dark/idea-tracker-collection.png)
{% /only_dark %}
{% only_light %}
![Create collection screen](/images/docs/tutorials/idea-tracker-collection.png)
{% /only_light %}
Create a new collection with the following attributes:
| Field | Type | Required | Size |
|-------------|--------|----------|----------|
| userId | String | Yes | 200 |
| title | String | Yes | 200 |
| description | String | No | 500 |
Change the collection's permissions in the settings to give access.
{% only_dark %}
![Collection permissions screen](/images/docs/tutorials/dark/idea-tracker-permissions.png)
{% /only_dark %}
{% only_light %}
![Collection permissions screen](/images/docs/tutorials/idea-tracker-permissions.png)
{% /only_light %}
Navigate to the **Settings** tab of your collection, add the role **Any** and check the **Read** box.
Next, add a **Users** role and give them access to **Create**, **Update** and **Delete** by checking those boxes.
# Environment variables {% #environment-variables %}
Just like when we set up the connection to Appwrite in [step 3](/docs/tutorials/nuxt/step-3), we need to keep the variables with the collection id secret.
Open the `.env` file and add your database id and your collection id to it.
```
VITE_DATABASE_ID="YOUR_DATABASE_ID"
VITE_COLLECTION_ID="YOUR_COLLECTION_ID"
```
# Query methods {% #query-methods %}
Now that we have a collection in the database to hold ideas, we can connect to it from our app.
Our users should be able to read, add and remove ideas.
We will add a new composable, `useIdeas`, to handle this functionality.
Create a new file in the composables directory, `useIdeas.js` and add the following code.
```ts
// composables/useIdeas.ts
import { ID, Query, Models} from "appwrite";
import { database } from "~/appwrite";
import { ref } from "vue";
const ideasDatabaseId: string = import.meta.env.VITE_DATABASE_ID;
const ideasCollectionId: string = import.meta.env.VITE_COLLECTION_ID;
const queryLimit: number = 10;
interface Idea extends Models.Document{
title: string;
description: string;
userId: string;
}
const current = ref<Idea[] | null>(null); // Reference for the fetched data
export const useIdeas = () => {
// Fetch the 10 most recent ideas from the database
// Add the list to the current reference object
const fetch = async (): Promise<void> => {
const response = await database.listDocuments(
ideasDatabaseId,
ideasCollectionId,
[Query.orderDesc("$createdAt"), Query.limit(queryLimit)]
);
current.value = response.documents as Idea[];
};
// Add new idea to the database,
// Change the value of the current object
const add = async (idea: Idea): Promise<void> => {
const response = await database.createDocument(
ideasDatabaseId,
ideasCollectionId,
ID.unique(),
idea
);
current.value = [response, ...current.value as Idea[]].slice(0, 10) as Idea[];
};
const remove = async (id: string): Promise<void> => {
await database.deleteDocument(ideasDatabaseId, ideasCollectionId, id);
await fetch(); // Refetch ideas to ensure we have 10 items
};
fetch();
return {
add,
current,
fetch,
remove,
};
};
```
Now we can call the `useIdeas` composable from the home page.

View File

@@ -1,181 +0,0 @@
---
layout: tutorial
title: Ideas page
description: Add ideas from Appwrite database in your app.
step: 7
---
With the methods in the `useIdeas` composable we can get some ideas to the home page for the users to interact with.
We will use it in a form component so the logged in users can add their ideas, and in a list component to render the ten most recent ideas.
We start with building the form.
# Idea form {% #idea-form %}
On the home page, the logged in users should be able to add their ideas to the Appwrite database.
The form need a text field for filling in the title, a textarea for the description and a submit button.
From the `components` directory, add the file `IdeasForm.vue` and add the following code.
```vue
<!-- components/IdeasForm.vue -->
<script setup>
const ideas = useIdeas();
const user = useUserSession();
const handleAddIdea = async (event) => {
const form = event.target;
const formData = new FormData(form);
// Extract the values from the FormData object and add userId
const postIdeaData = {
userId: user.current.value.userId,
title: formData.get('title'),
description: formData.get('description'),
};
await ideas.add(postIdeaData);
form.reset(); // Clear the form
};
</script>
<template>
<div>
<article class="container padding-0">
<h4 class="heading-level-4">Submit Idea</h4>
<form @submit.prevent="handleAddIdea" class="u-margin-block-start-16">
<ul class="form-list">
<li class="form-item">
<label class="label">Title</label>
<input
type="text"
placeholder="Title"
name="title"
/>
</li>
<li class="form-item">
<label class="label">Description</label>
<textarea
placeholder="Description"
name="description"
/>
</li>
<button class="button" aria-label="Submit idea" type="submit">
Submit
</button>
</ul>
</form>
</article>
</div>
</template>
```
Next, add the component to the page `pages/index.vue` by auto-importing it in the `<template>` tag.
In doing that, we need to take a moment to think about how we want to display the form to the users.
Since it should only be shown to logged in user, we need to wrap it in a `<section>` that renders conditionally when the `isLoggedIn` reference in the `useUserSession` is true.
If the requirement is not met, we show a paragraph with some information to the user instead.
Add the following code to the `index.vue` page to conditionally render the form and information paragraph.
Overwrite the contents of `pages/index.vue` with the following code.
```vue
<!-- pages/index.vue -->
<script setup>
const user = useUserSession();
</script>
<template>
<div class="u-max-width-650" style="margin-inline: auto;">
<!-- Idea form component for logged in users -->
<section v-if="user.current.value" class="card u-margin-32">
<IdeasForm />
</section>
<section v-else class="card u-margin-32">
<div class="container">
<p class="body-text-1" style="width: 100%;">
Please login to submit an idea.
</p>
</div>
</section>
<IdeasList />
</div>
</template>
<style>
article.box {
background-color: hsl(var(--color-neutral-0));
}
</style>
````
# Ideas list {% #ideas-list %}
Now that we can get some ideas to show, we go on to build the component for the list of ideas.
Once again, we need to take a moment to think about how this component should work.
First of all, the ideas should be visible for the users before any interaction has taken place on the page.
To catch that moment in time when the page loads, we call our `fetch` function, that fetches the ideas in Appwrite, from the built-in `onMounted` function.
Second, it's likely that a user will want to delete one of their ideas from the database.
We help them do that by adding a delete button in the top right corner of the idea list item, but only on the ideas added by the user itself.
Add the file `IdeasList` from the `componenents` directory and insert the following code:
```vue
<!-- componenents/IdeasList.vue -->
<script setup>
import { onMounted } from 'vue';
const ideas = useIdeas();
const user = useUserSession();
</script>
<template>
<section class="u-margin-32">
<article class="card">
<h4 class="heading-level-4">Latest Ideas</h4>
<ul class="u-margin-block-start-8">
<template v-if="ideas.current.value && ideas.current.value.length">
<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 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" v-if="user.current.value &&
idea.userId === user.current.value.userId
" type="button" @click="ideas.remove(idea.$id)">
<span class="icon-document-remove" aria-hidden="true" />
</button>
</div>
</div>
</li>
</template>
<template v-else>
<p>No ideas yet.</p>
</template>
</ul>
</article>
</section>
</template>
```
Return to the file `pages/index.vue` once more to import the list of ideas.
This component should be visible to all users, so no conditional rendering neeeds to be handled.
```vue
<!-- pages/index.vue -->
<template>
<div class="u-max-width-650" style="margin: 0 auto;">
... Some skipped code
<IdeasList />
</div>
</template>
.. Skipped script section and styling
````
Congratulations!
You now have an ideas tracker built with Nuxt and Appwrite to use locally.

View File

@@ -1,11 +0,0 @@
---
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` and open the URL shown by the NPM command in your browser.
Head to the [Appwrite Console](https://cloud.appwrite.io/console) to see the new users and follow their interactions.