mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
doc: add tutorial example
This commit is contained in:
+2
-1
@@ -7,5 +7,6 @@ members = [
|
||||
]
|
||||
exclude = [
|
||||
"examples/playground",
|
||||
"examples/tuono"
|
||||
"examples/tuono",
|
||||
"examples/tutorial"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.tuono
|
||||
out
|
||||
target
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = {version = "0.7.5", features = ["json"]}
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
tower-http = {version = "0.5.2", features = ["fs"]}
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
tuono_lib = { path = "../../crates/tuono_lib/"}
|
||||
serde_json = "1.0"
|
||||
reqwest = {version = "0.12.4", features = ["json"]}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Tuono starter
|
||||
|
||||
This is the starter tuono project. To download it run in your terminal:
|
||||
|
||||
```shell
|
||||
$ tuono new [NAME]
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "tuono-tutorial",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"tuono": "link:../../packages/tuono"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="82" height="73" viewBox="0 0 82 73" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M81.337 36.6169C81.337 31.245 74.5945 26.1541 64.2571 22.9971C66.6426 12.4847 65.5824 4.1211 60.9107 1.44343C59.8339 0.815331 58.5748 0.517811 57.1998 0.517811V4.20375C57.9619 4.20375 58.5748 4.35251 59.0884 4.6335C61.3414 5.92275 62.3188 10.8318 61.5568 17.1458C61.3745 18.6996 61.0763 20.3359 60.7119 22.0053C57.4649 21.212 53.9197 20.6004 50.1923 20.2037C47.9558 17.1459 45.6365 14.369 43.3007 11.9393C48.7013 6.93101 53.7706 4.18722 57.2164 4.18722V0.501282C52.6607 0.501282 46.6967 3.74094 40.6666 9.36076C34.6364 3.774 28.6726 0.567398 24.1168 0.567398V4.25333C27.5461 4.25333 32.6319 6.9806 38.0326 11.9558C35.7133 14.3855 33.394 17.1459 31.1907 20.2037C27.4467 20.6004 23.9015 21.212 20.6545 22.0219C20.2735 20.369 19.9918 18.7657 19.793 17.2285C19.0144 10.9145 19.9752 6.0054 22.2117 4.69962C22.7087 4.4021 23.3548 4.26987 24.1168 4.26987V0.583927C22.7252 0.583927 21.4662 0.881447 20.3728 1.50954C15.7177 4.18722 14.674 12.5343 17.0761 23.0136C6.77188 26.1871 0.0625 31.2615 0.0625 36.6169C0.0625 41.9887 6.805 47.0796 17.1424 50.2366C14.7568 60.749 15.8171 69.1126 20.4888 71.7903C21.5656 72.4184 22.8246 72.7159 24.2162 72.7159C28.772 72.7159 34.7359 69.4762 40.766 63.8564C46.7962 69.4432 52.76 72.6498 57.3158 72.6498C58.7073 72.6498 59.9664 72.3523 61.0598 71.7242C65.7149 69.0465 66.7586 60.6994 64.3565 50.2201C74.6276 47.0631 81.337 41.9722 81.337 36.6169ZM59.7676 25.5921C59.1547 27.7243 58.3926 29.9227 57.5311 32.121C56.8519 30.7987 56.1396 29.4764 55.361 28.1541C54.5989 26.8318 53.7872 25.5425 52.9754 24.2863C55.3278 24.6334 57.5974 25.0632 59.7676 25.5921ZM52.1802 43.1953C50.888 45.4267 49.5628 47.5424 48.1877 49.5094C45.7194 49.7243 43.2178 49.84 40.6997 49.84C38.1982 49.84 35.6967 49.7242 33.2449 49.5259C31.8699 47.559 30.528 45.4598 29.2358 43.2449C27.9768 41.0797 26.8337 38.8813 25.79 36.6664C26.8172 34.4516 27.9768 32.2367 29.2193 30.0714C30.5114 27.84 31.8368 25.7243 33.2118 23.7574C35.6801 23.5425 38.1817 23.4268 40.6997 23.4268C43.2013 23.4268 45.7028 23.5425 48.1546 23.7409C49.5296 25.7078 50.8715 27.807 52.1637 30.0218C53.4227 32.1871 54.5658 34.3855 55.6094 36.6003C54.5658 38.8152 53.4227 41.0301 52.1802 43.1953ZM57.5311 41.0466C58.4257 43.2615 59.1878 45.4763 59.8173 47.6251C57.6471 48.154 55.361 48.6003 52.992 48.9474C53.8037 47.6747 54.6155 46.3689 55.3775 45.0301C56.1396 43.7077 56.8519 42.3689 57.5311 41.0466ZM40.7329 58.6829C39.1922 57.0961 37.6515 55.3275 36.1274 53.3937C37.6184 53.4598 39.1425 53.5094 40.6832 53.5094C42.2404 53.5094 43.7811 53.4763 45.2886 53.3937C43.7976 55.3275 42.257 57.0961 40.7329 58.6829ZM28.4075 48.9474C26.0551 48.6003 23.7855 48.1705 21.6153 47.6416C22.2283 45.5094 22.9903 43.311 23.8518 41.1127C24.531 42.435 25.2434 43.7573 26.022 45.0796C26.8006 46.402 27.5958 47.6912 28.4075 48.9474ZM40.6501 14.5508C42.1907 16.1376 43.7314 17.9062 45.2555 19.8401C43.7645 19.7739 42.2404 19.7244 40.6997 19.7244C39.1425 19.7244 37.6018 19.7574 36.0943 19.8401C37.5853 17.9062 39.126 16.1376 40.6501 14.5508ZM28.391 24.2863C27.5792 25.559 26.7675 26.8648 26.0054 28.2037C25.2433 29.526 24.531 30.8483 23.8518 32.1706C22.9572 29.9557 22.1951 27.7409 21.5656 25.5921C23.7358 25.0797 26.022 24.6334 28.391 24.2863ZM13.3984 44.9805C7.53392 42.4846 3.74023 39.2119 3.74023 36.6169C3.74023 34.0218 7.53392 30.7326 13.3984 28.2533C14.8231 27.6417 16.3803 27.0962 17.9873 26.5838C18.9316 29.8235 20.1741 33.1954 21.7147 36.6499C20.1906 40.0879 18.9647 43.4433 18.037 46.6664C16.3969 46.154 14.8397 45.592 13.3984 44.9805ZM22.3111 68.6002C20.0581 67.311 19.0807 62.4019 19.8427 56.0879C20.0249 54.5341 20.3231 52.8978 20.6876 51.2284C23.9346 52.0218 27.4798 52.6333 31.2072 53.03C33.4437 56.0879 35.763 58.8647 38.0988 61.2945C32.6982 66.3027 27.6289 69.0465 24.1831 69.0465C23.4376 69.03 22.8081 68.8812 22.3111 68.6002ZM61.6065 56.0052C62.3851 62.3192 61.4242 67.2283 59.1878 68.5341C58.6908 68.8316 58.0447 68.9639 57.2827 68.9639C53.8534 68.9639 48.7675 66.2366 43.3669 61.2614C45.6862 58.8317 48.0055 56.0713 50.2088 53.0135C53.9528 52.6168 57.498 52.0052 60.745 51.1953C61.1261 52.8647 61.4242 54.468 61.6065 56.0052ZM67.9845 44.9805C66.5598 45.592 65.0026 46.1375 63.3956 46.6499C62.4513 43.4102 61.2089 40.0383 59.6682 36.5838C61.1923 33.1458 62.4182 29.7904 63.3459 26.5673C64.986 27.0797 66.5432 27.6417 68.0011 28.2533C73.8656 30.7491 77.6592 34.0218 77.6592 36.6169C77.6427 39.2119 73.849 42.5011 67.9845 44.9805Z" fill="#61DAFB"/>
|
||||
<path d="M40.6832 44.1706C44.8644 44.1706 48.254 40.7886 48.254 36.6169C48.254 32.4451 44.8644 29.0632 40.6832 29.0632C36.502 29.0632 33.1124 32.4451 33.1124 36.6169C33.1124 40.7886 36.502 44.1706 40.6832 44.1706Z" fill="#61DAFB"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.3 KiB |
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user