diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 00000000..bf702b68 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,444 @@ +# Tuono tutorial + +This tutorial is meant for giving you a sneak peek of the framework and is intended to evolve along the development - be sure to have installed the latest version. + +The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction). + +## Installation + +The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal: + +```bash +$ cargo install tuono +``` + +To check that is correctly installed run: + +```bash +$ tuono --version +``` + +## Project scaffold + +To setup a new project you just need to run the following command: + +```bash +$ tuono new tuono-tutorial +``` + +Get into the project folder and install the dependencies with: + +```bash +$ pnpm install +``` + +Open it with your favourite code editor. + +The project will have the following structure: + +```bash +| public/ +- src/ + | routes/ + | styles/ +| package.json +| Cargo.toml +| .gitignore +| tsconfig.json +``` + +**public/**: put here all the files you want to be public + +**src/routes/**: All the files in this folder are considered routes. All the routes are server side rendered out of the box. To add server side capabilities just create a rust file with the same name as the route (i.e. `about.tsx` → `about.rs`). + +**src/styles/**: In this folder there is the `global.css` file that stores all the global styles. For the rest of the project you can use CSS modules (⚠️ CSS modules on routes are forbidden). + +## Start the dev environment + +To start the development environment you just need to run the following command within the project folder: + +```bash +$ tuono dev +``` +The first time might take a little bit because it will install all the rust’s dependencies. All the other execution will be pretty quick! + +> 💡 The `tuono dev` development script is currently under strong optimization improvements. In case you face any error delete the cache `.tuono` folder and run it again! + +Then open [`http://localhost:3000/`](http://localhost:3000/) on the browser. + +## The “/” route + +All the `index.tsx` files represent the folder root page (i.e. `src/routes/posts/index.tsx` is [`http://localhost:3000/posts`](http://localhost:3000/posts) as well as `src/routes/posts.tsx`). + +The file `index.rs` represents the server side capabilities for the index route. On this file you can: + +- Passing server side props +- Redirect/Rewrite to a different route (Available soon) +- Changing http status code (Available soon) + +## Tutorial introduction + +Now that we have some knowledge about the project structure let’s start the real tutorial. + +The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the pokemons of the first generation (the best one btw) and then reserve a dynamic page for each one separately. + +## Fetch all the pokemons + +To start let’s fetch all of them in the root page; since we want to render them on the server side we gonna need to implement the logic in the `index.rs` file. + +Clear the `index.rs` file and paste: + +```rust +// src/routes/index.rs +use serde::{Deserialize, Serialize}; +use tuono_lib::{Request, Response}; + +const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151"; + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemons { + results: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemon { + name: String, + url: String, +} + +#[tuono_lib::handler] +async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response { + + return match fetch.get(ALL_POKEMON).send().await { + Ok(res) => { + let data = res.json::().await.unwrap(); + Response::Props(Box::new(data)) + } + Err(_err) => Response::Props(Box::new(Pokemons { results: vec![] })), + }; + +} +``` + +Now the pokemons are correctly fetched and hydrated on the client side so we can actually use them. Clear the `index.tsx` file and paste: + +```tsx +// src/routes/index.tsx +import type { TuonoProps } from "tuono"; + +interface Pokemon { + name: string +} + +interface IndexProps { + results: Pokemon[] +} + +export default function IndexPage({ + data, +}: TuonoProps): JSX.Element { + if (!data?.results) { + return <>; + } + + return ( + <> +
+ + Crates + + + Npm + +
+
+

+ TUONO +

+
+ + +
+
+
    + {data.results.map((pokemon) => { + return pokemon.name; + })} +
+ + ); +} +``` + +Refresh now the browser! A bit ugly but all the pokemons are finally printed on screen! + +## Create a stand-alone component + +Let’s then create the button needed for displaying the list of pokemons. + +Create the following file `src/components/PokemonLink.tsx` and fill the content with: + +```tsx +// src/components/PokemonLink.tsx +import { Link } from "tuono"; + +interface Pokemon { + name: string +} + +export default function PokemonLink({ + pokemon, + id, +}: { + pokemon: Pokemon; + id: number; +}): JSX.Element { + return ( + + {pokemon.name} + + + ); +} +``` + +Now that the link is done let’s import it in the `index.tsx` file + +```diff +// src/routes/index.tsx + +++ import PokemonLink from '../components/PokemonLink' + +// ... +
    +-- {pokemons.map((pokemon) => { +-- return pokemon.name; +++ {pokemons.map((pokemon, i) => { +++ return ; + })} +
+// ... +``` + +Now the links work. Clicking on any of them we get redirected to the 404 page because we haven’t yet implemented the `pokemons/[pokemon]` page. +As previously said CSS modules are enabled out of the box so let’s make those links a little bit nicer. + +Create alongside the `PokemonLink.tsx` component the CSS module `PokemonLink.module.css` and copy the following content into it: + +```css +/* src/components/PokemonLink.module.css */ + +.link { + width: 100%; + max-width: 216px; + position: relative; + background: white; + margin-bottom: 10px; + border: solid #f0f0f0 1px; + text-decoration: none; + color: black; + padding: 5px 5px 5px 15px; + border-radius: 10px; + display: flex; + justify-content: space-between; + transition: 0.2s; + align-items: center; +} + +.link:hover { + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; +} + +.link img { + width: 70px; + background: white; + border-radius: 50%; +} +``` + +> 💡 SASS is supported out of the box. Just install the processor in the devDependencies `pnpm i -D sass` and run again `tuono dev` + +Then import the styles into the `PokemonLink` component as following: + +```diff +// src/components/PokemonLink.tsx +import { Link } from "tuono"; +import type { Pokemon } from "./../types/pokemon"; +++ import styles from './PokemonLink.module.css' + +export default function PokemonLink({ + pokemon, + id, +}: { + pokemon: Pokemon; + id: number; +}): JSX.Element { + return ( +-- +++ + {pokemon.name} + + + ); +} +``` + +## Create the `/pokemons/[pokemon]` route + +The homepage is ready. We have the full list of pokemons and they are all links. Great! + +Now we want to make those links actually pointing to a real page. Let’s create the dynamic route. + +Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`. + +These two will handle every requests that points to `http://localhost:3000/pokemons/bulbasaur..mew`. + +Let’s first work on the server side file. Paste into the new `[pokemon].rs` file the following code: + +```rust +use serde::{Deserialize, Serialize}; +use tuono_lib::{Request, Response}; + +const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon"; + +#[derive(Debug, Serialize, Deserialize)] +struct Pokemon { + name: String, + id: u16, + weight: u16, + height: u16, +} + +#[tuono_lib::handler] +async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response { + // The param `pokemon` is defined in the route filename [pokemon].rs + let pokemon = req.params.get("pokemon").unwrap(); + + return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await { + Ok(res) => { + let data = res.json::().await.unwrap(); + Response::Props(Box::new(data)) + } + Err(_err) => Response::Props(Box::new(Pokemon { + name: "Nope".to_string(), + id: 0, + weight: 0, + height: 0, + })), + }; + +} +``` + +Then let’s work on the frontend. Paste into the `[pokemon].tsx` file the following code: + +```tsx +import { TuonoProps } from "tuono"; +import PokemonView from "../../components/PokemonView"; + +export default function Pokemon({ data }: TuonoProps): JSX.Element { + return ; +} +``` + +The browser should complain that the component `PokemonView` does not exist. Let’s create it then! + +```tsx +// components/PokemonView.tsx +import { Link } from "tuono"; +import styles from "./PokemonView.module.css"; + +interface Pokemon { + name: string + id: string + weight: number + height: number +} + +export default function PokemonView({ + pokemon, +}: { + pokemon?: Pokemon; +}): JSX.Element { + return ( +
+ + Back + + {pokemon?.name && ( +
+
+

+ {pokemon.name} +

+
+
Weight:
+
{pokemon.weight}lbs
+
+
+
Height:
+
{pokemon.height}ft
+
+
+ +
+ )} +
+ ); +} +``` + +```css +/* components/PokemonView.module.css */ +.back-btn { + background-color: white; + border-radius: 10px; + padding: 7px 15px; + color: black; + text-decoration: none; + border: solid #f0f0f0 1px; + font-size: 20px; +} + +.back-btn:hover { + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; +} + +.pokemon { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.name { + font-size: 50px; + font-weight: 700; +} + +.pokemon img { + width: 400px; +} + +.spec { + display: flex; + font-size: 18px; + margin-top: 10px; +} + +.label { + font-weight: 700; +} +``` + +## Conclusion + +That’s it! You just created a multi thread full stack application with rust and react. + +The project is still under heavy development and the script for the production build (`tuono build`) is not ready yet but +I hope you got the taste of what is like working with rust and react in the same stack.