Files
tuono/docs/tutorial.md
T
2024-06-12 13:42:02 +02:00

12 KiB
Raw Blame History

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.

If you prefer to see directly the final outcome check out the tutorial source.

Installation

The tuono CLI is hosted on crates.io; to download and install it just run on a terminal:

$ cargo install tuono

To check that is correctly installed run:

$ tuono --version

Project scaffold

To setup a new project you just need to run the following command:

$ tuono new tuono-tutorial

Get into the project folder and install the dependencies with:

$ pnpm install

Open it with your favourite code editor.

The project will have the following structure:

├──  package.json
├──  public
├──  src
│   ├──  routes
│   └──  styles
├──  Cargo.toml
├──  README.md
└──  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.tsxabout.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:

$ tuono dev

The first time might take a little bit because it will install all the rusts 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/ 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 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 lets start the real tutorial.

The goal is to use the PokeAPI 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 lets 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:

// 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<Pokemon>,
}

#[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::<Pokemons>().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:

// src/routes/index.tsx
import type { TuonoProps } from "tuono";

interface Pokemon {
  name: string
}

interface IndexProps {
  results: Pokemon[]
}

export default function IndexPage({
  data,
}: TuonoProps<IndexProps>): JSX.Element {
  if (!data?.results) {
    return <></>;
  }

  return (
    <>
      <header className="header">
        <a href="https://crates.io/crates/tuono" target="_blank">
          Crates
        </a>
        <a href="https://www.npmjs.com/package/tuono" target="_blank">
          Npm
        </a>
      </header>
      <div className="title-wrap">
        <h1 className="title">
          TU<span>O</span>NO
        </h1>
        <div className="logo">
          <img src="rust.svg" className="rust" />
          <img src="react.svg" className="react" />
        </div>
      </div>
      <ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
        {data.results.map((pokemon) => {
          return pokemon.name;
        })}
      </ul>
    </>
  );
}

Refresh now the browser! A bit ugly but all the pokemons are finally printed on screen!

Create a stand-alone component

Lets then create the button needed for displaying the list of pokemons.

Create the following file src/components/PokemonLink.tsx and fill the content with:

// 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 (
    <Link href={`/pokemons/${pokemon.name}`}>
      {pokemon.name}
      <img
        src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
      />
    </Link>
  );
}

Now that the link is done lets import it in the index.tsx file

// src/routes/index.tsx

++  import PokemonLink from '../components/PokemonLink'

// ...
	<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
--      {pokemons.map((pokemon) => {
--        return pokemon.name;
++      {pokemons.map((pokemon, i) => {
++        return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />;
      })}
	</ul>
// ...

Now the links work. Clicking on any of them we get redirected to the 404 page because we havent yet implemented the pokemons/[pokemon] page. As previously said CSS modules are enabled out of the box so lets 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:

/* 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:

// 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 (
--    <Link href={`/pokemons/${pokemon.name}`}>
++    <Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
      {pokemon.name}
      <img
        src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
      />
    </Link>
  );
}

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. Lets 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.

Lets first work on the server side file. Paste into the new [pokemon].rs file the following code:

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::<Pokemon>().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 lets work on the frontend. Paste into the [pokemon].tsx file the following code:

import { TuonoProps } from "tuono";
import PokemonView from "../../components/PokemonView";

export default function Pokemon({ data }: TuonoProps): JSX.Element {
  return <PokemonView pokemon={data} />;
}

The browser should complain that the component PokemonView does not exist. Lets create it then!

// 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 (
    <div>
      <Link className={styles["back-btn"]} href="/">
        Back
      </Link>
      {pokemon?.name && (
        <div className={styles.pokemon}>
          <div>
            <h1 className={styles.name}>
              {pokemon.name}
            </h1>
            <dl className={styles.spec}>
              <dt className={styles.label}>Weight:</dt>
              <dd>{pokemon.weight}lbs</dd>
            </dl>
            <dl className={styles.spec}>
              <dt className={styles.label}>Height:</dt>
              <dd>{pokemon.height}ft</dd>
            </dl>
          </div>
          <img
            src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
          />
        </div>
      )}
    </div>
  );
}
/* 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

Thats 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.