mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 21:02:45 -07:00
doc: add tutorial example
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
.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%;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Link } from 'tuono'
|
||||
import styles from './PokemonLink.module.css'
|
||||
|
||||
interface Pokemon {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function PokemonLink({
|
||||
pokemon,
|
||||
id,
|
||||
}: {
|
||||
pokemon: Pokemon
|
||||
id: number
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
|
||||
{pokemon.name}
|
||||
<img
|
||||
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
return <main className="main">{children}</main>
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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![] })),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// src/routes/index.tsx
|
||||
import type { TuonoProps } from 'tuono'
|
||||
import PokemonLink from '../components/PokemonLink'
|
||||
|
||||
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, i) => {
|
||||
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { TuonoProps } from 'tuono'
|
||||
import PokemonView from '../../components/PokemonView'
|
||||
|
||||
export default function Pokemon({ data }: TuonoProps): JSX.Element {
|
||||
return <PokemonView pokemon={data} />
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fbfbfb;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 673px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: black;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
font-size: 200px;
|
||||
line-height: 200px;
|
||||
z-index: 0;
|
||||
letter-spacing: -2px;
|
||||
margin-left: -8px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title span {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 140px;
|
||||
height: 30px;
|
||||
border: solid 3px black;
|
||||
border-radius: 10px;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: white;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-left: 240px;
|
||||
position: relative;
|
||||
top: 25px;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.rust {
|
||||
animation: rotate 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.react {
|
||||
top: 33px;
|
||||
left: 28px;
|
||||
animation: rotate 6s linear infinite reverse;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.subtitle-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
Reference in New Issue
Block a user