Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tutorial steps #10

Merged
merged 2 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 42 additions & 25 deletions src/lib/layouts/DocsTutorial.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
<script lang="ts">
type Link = {
title: string;
href: string;
step: number;
};
import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
import type { TocItem } from './DocsArticle.svelte';

export let title: string;
export let toc: Array<
Link & {
children?: Array<Omit<Link, 'step'>>;
}
>;
export let nextStep: Link | undefined = undefined;
export let prevStep: Link | undefined = undefined;
export let toc: Array<TocItem>;
export let currentStep: number;

export let tutorials: Array<Tutorial>;

$: nextStep = tutorials.find((tutorial) => tutorial.step === currentStep + 1);
$: prevStep = tutorials.find((tutorial) => tutorial.step === currentStep - 1);
</script>

<main class="u-contents">
Expand Down Expand Up @@ -126,24 +123,44 @@
<h5 class="aw-references-menu-title aw-eyebrow">Tutorial Steps</h5>
</div>
<ol class="aw-references-menu-list">
{#each toc as parent}
{#each tutorials as tutorial}
<li class="aw-references-menu-item">
<a href={parent.href} class="aw-references-menu-link">
<span class="aw-numeric-badge">{parent.step}</span>
<span class="aw-caption-400">{parent.title}</span>
<a href={tutorial.href} class="aw-references-menu-link">
<span class="aw-numeric-badge">{tutorial.step}</span>
<span class="aw-caption-400">{tutorial.title}</span>
</a>
{#if parent.children}
<ol
class="aw-references-menu-list u-margin-block-start-16 u-margin-inline-start-32"
>
{#each parent.children as child}
{#if currentStep === tutorial.step}
{#each toc as parent}
<ol
class="aw-references-menu-list u-margin-block-start-16 u-margin-inline-start-32"
>
<li class="aw-references-menu-item">
<a href={child.href} class="aw-references-menu-link">
<span class="aw-caption-400">{child.title}</span>
<a
href={parent.href}
class="aw-references-menu-link"
class:is-selected={parent.selected}
>
{#if parent?.step}
<span class="aw-numeric-badge">{parent.step}</span>
{/if}
<span class="aw-caption-400">{parent.title}</span>
</a>
{#if parent.children}
<ol
class="aw-references-menu-list u-margin-block-start-16 u-margin-inline-start-32"
>
{#each parent.children as child}
<li class="aw-references-menu-item">
<a href={child.href} class="aw-references-menu-link">
<span class="aw-caption-400">{child.title}</span>
</a>
</li>
{/each}
</ol>
{/if}
</li>
{/each}
</ol>
</ol>
{/each}
{/if}
</li>
{/each}
Expand Down
22 changes: 22 additions & 0 deletions src/lib/utils/tutorials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { base } from '$app/paths';
import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';

export function globToTutorial(data: { tutorials: Record<string, unknown>; pathname: string }) {
return Object.entries(data.tutorials)
.map(([filepath, tutorial]) => {
const { frontmatter } = tutorial as {
frontmatter: Tutorial;
};
const slug = filepath.replace('./', '').replace('/+page.markdoc', '');
const tutorialName = data.pathname.split('/').slice(0, -1).join('/');

return {
title: frontmatter.title,
step: frontmatter.step,
href: `${base}${tutorialName}/${slug}`
};
})
.sort((a, b) => {
return a.step - b.step;
});
}
53 changes: 38 additions & 15 deletions src/markdoc/layouts/Tutorial.svelte
Original file line number Diff line number Diff line change
@@ -1,39 +1,62 @@
<script context="module" lang="ts">
import { writable, type Writable } from 'svelte/store';

export type LayoutContext = Writable<
Record<
string,
{
title: string;
step?: number;
}
>
>;
export type Tutorial = {
title: string;
step: number;
href: string;
};
</script>

<script lang="ts">
import { Docs, DocsTutorial } from '$lib/layouts';
import { getContext, setContext } from 'svelte';
import Sidebar from '$routes/docs/Sidebar.svelte';
import { DocsTutorial } from '$lib/layouts';
import { getContext, onMount, setContext } from 'svelte';
import { MainFooter } from '$lib/components';
import type { TocItem } from '$lib/layouts/DocsArticle.svelte';
import { writable } from 'svelte/store';
import type { LayoutContext } from './Article.svelte';

export let title: string;
export let description: string;
export let difficulty: string;
export let readtime: string;
export let step: number;
export let tutorial: string;

setContext<LayoutContext>('headings', writable({}));

const tutorials = getContext<Tutorial[]>('tutorials');
const headings = getContext<LayoutContext>('headings');
let selected: string | undefined = undefined;
headings.subscribe((n) => {
const noVisible = Object.values(n).every((n) => !n.visible);
if (selected && noVisible) {
return;
}
for (const key in n) {
if (n[key].visible) {
selected = key;
break;
}
}
});

$: entries = Object.entries($headings);
$: toc = entries.reduce<Array<TocItem>>((carry, [id, heading]) => {
carry.push({
title: heading.title,
href: `#${id}`,
step: heading.step,
selected: selected === id
});
return carry;
}, []);
</script>

<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
</svelte:head>

<DocsTutorial {title} toc={[]}>
<DocsTutorial {title} {toc} {tutorials} currentStep={step}>
<svelte:fragment slot="metadata">
{#if difficulty}
<li>{difficulty}</li>
Expand Down
8 changes: 8 additions & 0 deletions src/routes/docs/tutorials/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
import { setContext } from 'svelte';

setContext<Tutorial[]>('tutorials', []);
</script>

<slot />
1 change: 1 addition & 0 deletions src/routes/docs/tutorials/+layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const prerender = true;
10 changes: 10 additions & 0 deletions src/routes/docs/tutorials/test/+layout.svelte
Original file line number Diff line number Diff line change
@@ -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 />
11 changes: 11 additions & 0 deletions src/routes/docs/tutorials/test/+layout.ts
Original file line number Diff line number Diff line change
@@ -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
};
};
6 changes: 6 additions & 0 deletions src/routes/docs/tutorials/test/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

export const load: PageLoad = async () => {
throw redirect(303, '/docs/tutorials');
};
155 changes: 155 additions & 0 deletions src/routes/docs/tutorials/test/introduction/+page.markdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
layout: tutorial
title: Introduction
description: This is the description used for SEO.
step: 1
difficulty: easy
readtime: 10
---

Email and password login is the most commonly used authentication method. Appwrite Authentication promotes a safer internet by providing secure APIs and promoting better password choices to end users. Appwrite supports added security features like blocking personal info in passwords, password dictionary, and password history to help users choose good passwords.

## Sign up {% #sign-up %}

You can use the Appwrite Client SDKs to create an account using email and password.

```js
import { Client, Account, ID } from "appwrite";

const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('[PROJECT_ID]'); // Your project ID

const account = new Account(client);

const promise = account.create('[USER_ID]', '[email protected]', '');

promise.then(function (response) {
console.log(response); // Success
}, function (error) {
console.log(error); // Failure
});
```

Passwords are hashed with [Argon2](#), a resilient and secure password hashing algorithm.

## Verification {% #verification %}

After an account is created, it can be verified through the account create verification route. The user doesn't need to be verified to log in, but you can restrict resource access to verified users only using permissions through the `user([USER_ID], "verified")` role.

First, send a verification email. Specify a redirect URL which users will be redirected to. The verification secrets will be appended as query parameters to the redirect URL. In this example, the redirect URL is `https://example.com/verify`.

```js
import { Client, Account } from "appwrite";

const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2') // Your project ID

const account = new Account(client);

const promise = account.createVerification('https://example.com');

promise.then(function (response) {
console.log(response);
}, function (error) {
console.log(error);
});
```

Next, implement the verification page in your app. This page will parse the secrets passed in through the `userId` and `secret` query parameters. In this example, the code below will be found in the page served at `https://example.com/verify`.

Since the secrets are passed in through url params, it will be easiest to perform this step in the browser.

```js
import { Client, Account } from "appwrite";

const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('[PROJECT_ID]'); // Your project ID

const account = new Account(client);

const urlParams = new URLSearchParams(window.location.search);
const secret = urlParams.get('secret');
const userId = urlParams.get('userId');

const promise = account.updateVerification(userId, secret);

promise.then(function (response) {
console.log(response);
}, function (error) {
console.log(error);
});
```

## Login {% #login %}

After you've created your account, users can be logged in using the Create Email Session route.

```js
import { Client, Account } from "appwrite";

const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('[PROJECT_ID]'); // Your project ID

const account = new Account(client);

const promise = account.createEmailSession('[email protected]', 'password');

promise.then(function (response) {
console.log(response); // Success
}, function (error) {
console.log(error); // Failure
});
```

## Password Recovery {% #password-recovery %}

If a user forgets their password, they can initiate a password recovery flow to recover their password. The Create Password Recovery endpoint sends the user an email with a temporary secret key for password reset. When the user clicks the confirmation link, they are redirected back to the password reset URL with the secret key and email address values attached to the URL as query strings.

Only redirect URLs to domains added as a platform on your Appwrite console will be accepted. URLs not added as a platform are rejected to protect against redirect attacks.

```js
import { Client, Account } from "appwrite";

const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('[PROJECT_ID]'); // Your project ID

const promise = account.createRecovery('[email protected]', 'https://example.com');

promise.then(function (response) {
console.log(response); // Success
}, function (error) {
console.log(error); // Failure
});
```

After receiving an email with the secret attached to the redirect link, submit a request to the Create Password Recovery (confirmation) endpoint to complete the recovery flow. The verification link sent to the user's email address is valid for 1 hour.

```js
import { Client, Account } from "appwrite";

const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('[PROJECT_ID]'); // Your project ID

const promise = account.updateRecovery(
'[USER_ID]',
'[SECRET]',
'password',
'password'
);

promise.then(function (response) {
console.log(response); // Success
}, function (error) {
console.log(error); // Failure
});
```

## Security {% #security %}

Appwrite's security first mindset goes beyond a securely implemented of authentication API. You can enable features like password dictionary, password history, and disallow personal data in passwords to encourage users to pick better passwords. By enabling these features, you protect user data and teach better password choices, which helps make the internet a safer place.
12 changes: 12 additions & 0 deletions src/routes/docs/tutorials/test/step-2/+page.markdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
layout: tutorial
title: Step 2
description: This is the description used for SEO.
step: 2
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

## Hallo {% #hallo %}

This is the second step :)
Loading