diff --git a/Cargo.toml b/Cargo.toml index b4ba7ebb..f5a3efbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ members = [ ] exclude = [ "examples/playground", - "examples/tuono" + "examples/tuono", + "examples/tutorial" ] diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore new file mode 100644 index 00000000..071acad0 --- /dev/null +++ b/examples/tutorial/.gitignore @@ -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 diff --git a/examples/tutorial/Cargo.toml b/examples/tutorial/Cargo.toml new file mode 100644 index 00000000..5ba88e75 --- /dev/null +++ b/examples/tutorial/Cargo.toml @@ -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"]} + diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md new file mode 100644 index 00000000..72b0fe6d --- /dev/null +++ b/examples/tutorial/README.md @@ -0,0 +1,9 @@ +# Tuono starter + +This is the starter tuono project. To download it run in your terminal: + +```shell +$ tuono new [NAME] +``` + + diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json new file mode 100644 index 00000000..b8745ca3 --- /dev/null +++ b/examples/tutorial/package.json @@ -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" + } +} diff --git a/examples/tutorial/public/favicon.ico b/examples/tutorial/public/favicon.ico new file mode 100644 index 00000000..d03bcc66 Binary files /dev/null and b/examples/tutorial/public/favicon.ico differ diff --git a/examples/tutorial/public/react.svg b/examples/tutorial/public/react.svg new file mode 100644 index 00000000..8fe604f2 --- /dev/null +++ b/examples/tutorial/public/react.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/tutorial/public/rust.svg b/examples/tutorial/public/rust.svg new file mode 100644 index 00000000..7f66780e --- /dev/null +++ b/examples/tutorial/public/rust.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/tutorial/src/components/PokemonLink.module.css b/examples/tutorial/src/components/PokemonLink.module.css new file mode 100644 index 00000000..d134c987 --- /dev/null +++ b/examples/tutorial/src/components/PokemonLink.module.css @@ -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%; +} diff --git a/examples/tutorial/src/components/PokemonLink.tsx b/examples/tutorial/src/components/PokemonLink.tsx new file mode 100644 index 00000000..4e7c8b9f --- /dev/null +++ b/examples/tutorial/src/components/PokemonLink.tsx @@ -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 ( + + {pokemon.name} + + + ) +} diff --git a/examples/tutorial/src/components/PokemonView.module.css b/examples/tutorial/src/components/PokemonView.module.css new file mode 100644 index 00000000..9b8f6f70 --- /dev/null +++ b/examples/tutorial/src/components/PokemonView.module.css @@ -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; +} diff --git a/examples/tutorial/src/components/PokemonView.tsx b/examples/tutorial/src/components/PokemonView.tsx new file mode 100644 index 00000000..0f643aab --- /dev/null +++ b/examples/tutorial/src/components/PokemonView.tsx @@ -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 ( +
+ + Back + + {pokemon?.name && ( +
+
+

{pokemon.name}

+
+
Weight:
+
{pokemon.weight}lbs
+
+
+
Height:
+
{pokemon.height}ft
+
+
+ +
+ )} +
+ ) +} diff --git a/examples/tutorial/src/routes/__root.tsx b/examples/tutorial/src/routes/__root.tsx new file mode 100644 index 00000000..1a39e91f --- /dev/null +++ b/examples/tutorial/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +interface RootRouteProps { + children: ReactNode +} + +export default function RootRoute({ children }: RootRouteProps): JSX.Element { + return
{children}
+} diff --git a/examples/tutorial/src/routes/index.rs b/examples/tutorial/src/routes/index.rs new file mode 100644 index 00000000..25649768 --- /dev/null +++ b/examples/tutorial/src/routes/index.rs @@ -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, +} + +#[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![] })), + }; +} diff --git a/examples/tutorial/src/routes/index.tsx b/examples/tutorial/src/routes/index.tsx new file mode 100644 index 00000000..25105096 --- /dev/null +++ b/examples/tutorial/src/routes/index.tsx @@ -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): JSX.Element { + if (!data?.results) { + return <> + } + + return ( + <> +
+ + Crates + + + Npm + +
+
+

+ TUONO +

+
+ + +
+
+
    + {data.results.map((pokemon, i) => { + return + })} +
+ + ) +} diff --git a/examples/tutorial/src/routes/pokemons/[pokemon].rs b/examples/tutorial/src/routes/pokemons/[pokemon].rs new file mode 100644 index 00000000..1251e91e --- /dev/null +++ b/examples/tutorial/src/routes/pokemons/[pokemon].rs @@ -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::().await.unwrap(); + Response::Props(Box::new(data)) + } + Err(_err) => Response::Props(Box::new(Pokemon { + name: "Nope".to_string(), + id: 0, + weight: 0, + height: 0, + })), + }; +} diff --git a/examples/tutorial/src/routes/pokemons/[pokemon].tsx b/examples/tutorial/src/routes/pokemons/[pokemon].tsx new file mode 100644 index 00000000..7cf77395 --- /dev/null +++ b/examples/tutorial/src/routes/pokemons/[pokemon].tsx @@ -0,0 +1,6 @@ +import { TuonoProps } from 'tuono' +import PokemonView from '../../components/PokemonView' + +export default function Pokemon({ data }: TuonoProps): JSX.Element { + return +} diff --git a/examples/tutorial/src/styles/global.css b/examples/tutorial/src/styles/global.css new file mode 100644 index 00000000..84fc0842 --- /dev/null +++ b/examples/tutorial/src/styles/global.css @@ -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; +} diff --git a/examples/tutorial/tsconfig.json b/examples/tutorial/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/examples/tutorial/tsconfig.json @@ -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" }] +} diff --git a/examples/tutorial/tsconfig.node.json b/examples/tutorial/tsconfig.node.json new file mode 100644 index 00000000..97ede7ee --- /dev/null +++ b/examples/tutorial/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +}