├── src ├── components │ ├── mdx │ │ ├── mod.rs │ │ ├── center.rs │ │ └── youtube.rs │ ├── esta_semana_en_rust │ │ ├── mod.rs │ │ ├── blog_content.rs │ │ ├── header.rs │ │ └── layout.rs │ ├── mod.rs │ ├── icons │ │ ├── comments.rs │ │ ├── next.rs │ │ ├── linkedin.rs │ │ ├── website.rs │ │ ├── twitter.rs │ │ ├── github.rs │ │ ├── mod.rs │ │ └── logo_rust_page.rs │ ├── markdown_render.rs │ ├── button_link.rs │ ├── pagination_buttons.rs │ ├── header.rs │ ├── card_article.rs │ ├── layout.rs │ ├── feature_articles.rs │ └── blog_content.rs ├── pages │ ├── mod.rs │ ├── esta_semana_en_rust.rs │ ├── article_page.rs │ └── home.rs ├── models │ ├── mod.rs │ ├── devto_article.rs │ ├── hashnode_article.rs │ └── article.rs ├── ssg.rs ├── utils │ └── mod.rs ├── async_component.rs ├── render.rs ├── meta.rs └── main.rs ├── .gitignore ├── rust-toolchain.toml ├── clippy.toml ├── leptosfmt.toml ├── assets └── images │ ├── blog-rust.png │ ├── cargo-generate-cli.png │ ├── cargo-interactive.png │ ├── julie-y-vic-about.jpeg │ └── julie-y-vic-home.png ├── guide_ui └── previews │ ├── Preview-001.png │ └── Preview-002.png ├── preview_generator ├── assets │ ├── tag.png │ ├── user.png │ ├── banner.png │ ├── this_week.jpg │ ├── RustLangES.png │ ├── WorkSans-Bold.ttf │ └── WorkSans-Regular.ttf ├── src │ ├── components.rs │ ├── models.rs │ ├── components │ │ ├── circle_rect.rs │ │ ├── tags.rs │ │ └── rounded_rect.rs │ ├── utils.rs │ ├── main.rs │ ├── this_week.rs │ └── blog.rs └── Cargo.toml ├── .rusty-hook.toml ├── .github ├── dependabot.yml └── workflows │ ├── quality-gate.yml │ ├── deploy.yml │ ├── this_week_in_rust.yml │ └── pr-preview.yml ├── rustfmt.toml ├── server ├── Cargo.toml ├── package.json ├── .vscode └── settings.json ├── server.bat ├── tailwind.config.js ├── flake.nix ├── articles ├── bienvenido.md ├── fundamentals-ownership.md ├── cargo-generate.md ├── variables-y-declaraciones.md ├── hicimos-el-sitio-web-de-nuestra-boda-en-angular-y-rust-pk8.md ├── un-pequeno-paseo-por-rust-4lko.md └── strings.md ├── flake.lock ├── gen_translated.py ├── README.md └── input.css /src/components/mdx/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod center; 2 | pub mod youtube; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ***/target 2 | out 3 | .idea/ 4 | node_modules/ 5 | style/output.css 6 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-arguments-threshold = 100 2 | type-complexity-threshold = 1000 3 | -------------------------------------------------------------------------------- /src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod article_page; 2 | pub mod esta_semana_en_rust; 3 | pub mod home; 4 | -------------------------------------------------------------------------------- /leptosfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | tab_spaces = 4 3 | attr_value_brace_style = "WhenRequired" 4 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod article; 2 | pub mod devto_article; 3 | pub mod hashnode_article; 4 | -------------------------------------------------------------------------------- /assets/images/blog-rust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/assets/images/blog-rust.png -------------------------------------------------------------------------------- /src/components/esta_semana_en_rust/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blog_content; 2 | pub mod header; 3 | pub mod layout; 4 | -------------------------------------------------------------------------------- /guide_ui/previews/Preview-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/guide_ui/previews/Preview-001.png -------------------------------------------------------------------------------- /guide_ui/previews/Preview-002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/guide_ui/previews/Preview-002.png -------------------------------------------------------------------------------- /preview_generator/assets/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/tag.png -------------------------------------------------------------------------------- /preview_generator/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/user.png -------------------------------------------------------------------------------- /assets/images/cargo-generate-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/assets/images/cargo-generate-cli.png -------------------------------------------------------------------------------- /assets/images/cargo-interactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/assets/images/cargo-interactive.png -------------------------------------------------------------------------------- /assets/images/julie-y-vic-about.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/assets/images/julie-y-vic-about.jpeg -------------------------------------------------------------------------------- /assets/images/julie-y-vic-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/assets/images/julie-y-vic-home.png -------------------------------------------------------------------------------- /preview_generator/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/banner.png -------------------------------------------------------------------------------- /preview_generator/assets/this_week.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/this_week.jpg -------------------------------------------------------------------------------- /preview_generator/assets/RustLangES.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/RustLangES.png -------------------------------------------------------------------------------- /preview_generator/assets/WorkSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/WorkSans-Bold.ttf -------------------------------------------------------------------------------- /preview_generator/assets/WorkSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RustLangES/blog/HEAD/preview_generator/assets/WorkSans-Regular.ttf -------------------------------------------------------------------------------- /preview_generator/src/components.rs: -------------------------------------------------------------------------------- 1 | mod circle_rect; 2 | mod rounded_rect; 3 | mod tags; 4 | 5 | pub use circle_rect::*; 6 | pub use rounded_rect::*; 7 | pub use tags::*; 8 | -------------------------------------------------------------------------------- /.rusty-hook.toml: -------------------------------------------------------------------------------- 1 | [hooks] 2 | pre-commit = "cargo fmt --all && leptosfmt src && cargo clippy -- -D warnings" 3 | post-commit = "echo yay" 4 | 5 | [logging] 6 | verbose = true 7 | 8 | -------------------------------------------------------------------------------- /src/components/mdx/center.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, Children, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn Center(children: Children) -> impl IntoView { 6 | view! {
{children()}
} 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "cargo" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | commit-message: 10 | prefix: "chore" 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | reorder_imports = true 6 | reorder_modules = true 7 | remove_nested_parens = true 8 | merge_derives = true 9 | imports_granularity = "Crate" 10 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blog_content; 2 | pub mod button_link; 3 | pub mod pagination_buttons; 4 | 5 | mod header; 6 | pub use header::*; 7 | 8 | pub mod esta_semana_en_rust; 9 | pub mod icons; 10 | pub mod layout; 11 | pub mod mdx; 12 | 13 | pub mod card_article; 14 | pub mod feature_articles; 15 | pub mod markdown_render; 16 | -------------------------------------------------------------------------------- /server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "start" ] 4 | then 5 | nohup cargo watch -x run --shell "npx tailwindcss -i ./input.css -o ./out/output.css && cargo run" & 6 | nohup python3 -m http.server -d out & 7 | fi 8 | 9 | if [ "$1" = "stop" ] 10 | then 11 | 12 | kill -9 $(sudo netstat -npl --inet | awk '/:8000/' | awk -F "[ /]+" '{print $7}') 13 | pkill cargo-watch 14 | fi 15 | -------------------------------------------------------------------------------- /.github/workflows/quality-gate.yml: -------------------------------------------------------------------------------- 1 | name: Quality Gate 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - ".rusty-hook.toml" 10 | - ".github/workflows/**.yml" 11 | - "**/Makefile.toml" 12 | - "**.py" 13 | - "README.md" 14 | 15 | jobs: 16 | quality-gate: 17 | uses: RustLangES/workflows/.github/workflows/quality-gate-front.yml@main 18 | -------------------------------------------------------------------------------- /preview_generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "preview_generator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | imageproc = "0.23.0" 8 | serde = { version = "1.0.192", features = ["derive"] } 9 | gray_matter = { version = "0.2.6", default-features = false, features = [ 10 | "yaml", 11 | ] } 12 | image = { version = "0.24.7", default-features = false, features = [ 13 | "png", 14 | "jpeg", 15 | ] } 16 | rusttype = "0.9.3" 17 | -------------------------------------------------------------------------------- /preview_generator/src/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug, Default)] 4 | pub struct Article { 5 | pub title: String, 6 | pub description: String, 7 | #[serde(default)] 8 | pub author: Option, 9 | #[serde(default)] 10 | pub authors: Option>, 11 | #[serde(default)] 12 | pub slug: String, 13 | #[serde(default)] 14 | pub tags: Option>, 15 | #[serde(default)] 16 | pub number_of_week: Option, 17 | #[serde(default)] 18 | pub date: Option, 19 | #[serde(default)] 20 | pub date_string: Option, 21 | } 22 | -------------------------------------------------------------------------------- /src/components/icons/comments.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn CommentIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "fill-black".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 18 | 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-lang-es-blog" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | futures = "0.3.28" 8 | leptos = { version = "0.6", features = ["ssr", "experimental-islands"] } 9 | lol_html = "1.1.1" 10 | tokio = { version = "1", features = ["full"] } 11 | gray_matter = "0.2.6" 12 | chrono = { version = "0.4.31", features = ["serde", "unstable-locales"] } 13 | leptos-mdx = { git = "https://github.com/RustLangES/leptos-mdx.git" } 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = "1.0.107" 16 | once_cell = "1.18.0" 17 | rss = { version = "2.0.6", features = ["validation"] } 18 | futures-concurrency = "7.5.0" 19 | 20 | [dev-dependencies] 21 | rusty-hook = "0.11.2" 22 | -------------------------------------------------------------------------------- /src/components/icons/next.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn NextIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "fill-black".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leptos-ssg", 3 | "version": "1.0.0", 4 | "description": "![image](https://github.com/RustLangES/blog/assets/56278796/ba1ac759-3fda-4983-80d2-965398bf8d35)", 5 | "main": "tailwind.config.js", 6 | "dependencies": { 7 | "@tailwindcss/typography": "^0.5.10", 8 | "tailwindcss": "^3.4.1" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/RustLangES/blog.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/RustLangES/blog/issues" 21 | }, 22 | "homepage": "https://github.com/RustLangES/blog#readme" 23 | } 24 | -------------------------------------------------------------------------------- /src/components/mdx/youtube.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[allow(clippy::needless_pass_by_value)] 5 | #[must_use] 6 | pub fn Youtube(video: String) -> impl IntoView { 7 | view! { 8 |
9 | 18 |
19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emmet.includeLanguages": { 3 | "rust": "html", 4 | "*.rs": "html" 5 | }, 6 | "tailwindCSS.includeLanguages": { 7 | "rust": "html", 8 | "*.rs": "html" 9 | }, 10 | "files.associations": { 11 | "*.rs": "rust" 12 | }, 13 | "editor.quickSuggestions": { 14 | "other": "on", 15 | "comments": "on", 16 | "strings": true 17 | }, 18 | "css.validate": false, 19 | "rust-analyzer.rustfmt.overrideCommand": [ 20 | "leptosfmt", 21 | "--stdin", 22 | "--rustfmt" 23 | ], 24 | "rust-analyzer.check.command": "clippy", 25 | "rust-analyzer.checkOnSave": true, 26 | "search.exclude": { 27 | "**/node_modules": true, 28 | "**/target": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /preview_generator/src/components/circle_rect.rs: -------------------------------------------------------------------------------- 1 | use image::{Rgba, RgbaImage}; 2 | use imageproc::drawing::{draw_filled_ellipse_mut, draw_filled_rect_mut}; 3 | use imageproc::rect::Rect; 4 | 5 | pub fn circle_rect(img: &mut RgbaImage, color: Rgba, (x, y): (i32, i32), (w, h): (i32, i32)) { 6 | let half_rad = h / 2; 7 | // let mut img = img 8 | // .view(pos.0 as u32, pos.1 as u32, size.0 as u32, size.1 as u32) 9 | // .to_image(); 10 | // Left 11 | draw_filled_ellipse_mut(img, (x, y + half_rad), half_rad, half_rad, color); 12 | // Right 13 | draw_filled_ellipse_mut( 14 | img, 15 | (x + w, y + half_rad), 16 | half_rad, 17 | half_rad, 18 | color, 19 | ); 20 | 21 | // main box 22 | draw_filled_rect_mut( 23 | img, 24 | Rect::at(x, y).of_size(w as u32, h as u32 + 2), 25 | color, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/markdown_render.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | use leptos_mdx::mdx::{Components, Mdx, MdxComponentProps}; 3 | 4 | use crate::components::mdx::{ 5 | center::{Center, CenterProps}, 6 | youtube::{Youtube, YoutubeProps}, 7 | }; 8 | 9 | #[component] 10 | pub fn MarkdownRender(content: String) -> impl IntoView { 11 | let mut components = Components::new(); 12 | components.add_props( 13 | "youtube".to_string(), 14 | Youtube, 15 | |props: MdxComponentProps| { 16 | let video_id = props.attributes.get("video").unwrap().clone(); 17 | YoutubeProps { 18 | video: video_id.unwrap(), 19 | } 20 | }, 21 | ); 22 | components.add_props("center".to_string(), Center, |props: MdxComponentProps| { 23 | CenterProps { 24 | children: props.children, 25 | } 26 | }); 27 | 28 | view! { 29 | <> 30 | 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | if "%~1"=="start" ( 5 | start /B cmd /C "cargo watch -x run && npx tailwindcss -i .\input.css -o .\out\output.css && cargo run" 6 | start /B python -m http.server --directory out 7 | ) 8 | 9 | if "%~1"=="stop" ( 10 | set "prevServer=" 11 | for /f "tokens=5" %%a in ('netstat -ano ^| findstr :8000 ^| find "LISTENING"') do ( 12 | set "server=%%~a" 13 | if not "!server!"=="!prevServer!" ( 14 | @REM echo !server! 15 | taskkill /PID !server! /F 16 | ) 17 | set "prevServer=!server!" 18 | ) 19 | 20 | :: Find and kill the process named cargo-watch 21 | :: image name == executable file name 22 | for /f "tokens=2 delims=," %%b in ('tasklist /nh /fo csv /fi "imagename eq cargo-watch.exe"') do ( 23 | set "watch=%%~b" 24 | @REM echo !watch! 25 | taskkill /PID !watch! /F 26 | ) 27 | 28 | echo Cargo-watch processes have been terminated. 29 | pause 30 | ) -------------------------------------------------------------------------------- /src/components/icons/linkedin.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn LinkedinIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "fill-black".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/icons/website.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn WebsiteIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "fill-black".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ssg.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use tokio::fs; 3 | 4 | use leptos::{provide_context, IntoView}; 5 | 6 | use crate::render; 7 | 8 | pub struct Ssg<'a> { 9 | out_dir: &'a Path, 10 | } 11 | 12 | impl<'a> Ssg<'a> { 13 | #[must_use] 14 | pub fn new(out_dir: &'a Path) -> Self { 15 | Self { out_dir } 16 | } 17 | 18 | /// # Errors 19 | /// This can return an error if `fs::write` fails. 20 | pub async fn gen( 21 | &'a self, 22 | path: String, 23 | view: F, 24 | ) -> Result<(), Box> 25 | where 26 | F: FnOnce() -> V + 'static, 27 | V: IntoView, 28 | { 29 | // SsgContext will be available to all components in the view 30 | let ssg_ctx = SsgContext { path: path.clone() }; 31 | 32 | // Render the view to a string 33 | let res = 34 | render::render(move || view().into_view(), move || provide_context(ssg_ctx)).await; 35 | 36 | // Write the string to a file 37 | let out_file = self.out_dir.join(path); 38 | fs::write(&out_file, res).await?; 39 | println!("wrote {}", out_file.display()); 40 | 41 | Ok(()) 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct SsgContext { 47 | pub path: String, 48 | } 49 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: { 6 | files: ["*.html", "./src/**/*.rs"], 7 | }, 8 | theme: { 9 | fontFamily: { 10 | "alfa-slab": ["Alfa Slab One", "sans-serif"], 11 | "fira-sans": ["Fira Sans", "sans-serif"], 12 | "work-sans": ["Work Sans", "sans-serif"], 13 | }, 14 | extend: { 15 | screens: { 16 | 'xs': '475px', 17 | ...defaultTheme.screens, 18 | }, 19 | fontSize: { 20 | 'half': '3.5rem', 21 | }, 22 | lineHeight: { 23 | '3lh': '2.8rem', 24 | }, 25 | backgroundImage: (theme) => ({ 26 | "anuncios": "url('https://i.imgur.com/tDlT9sr.jpg')", 27 | "kaku-dev": "url('https://rustlang-es.org/kaku.avif')", 28 | "kaku": "url('https://rustlang-es.org/kaku.avif')", 29 | }), 30 | gridTemplateColumns: (theme) => ({ 31 | "divided": "2.5fr 1fr", 32 | "sidebar-article": "5rem 1fr" 33 | }), 34 | fill: (theme) => ({ 35 | "shape-fill-light": "rgb(203 213 225 / 1)", 36 | "shape-fill-dark": "rgb(39 39 42 / 1)", 37 | }), 38 | }, 39 | }, 40 | plugins: [ 41 | require('@tailwindcss/typography'), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/icons/twitter.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn TwitterIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "fill-black".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/icons/github.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn GithubIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "fill-black".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/esta_semana_en_rust.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | use crate::{ 4 | components::esta_semana_en_rust::{blog_content::BlogContent, layout::Layout}, 5 | models::article::Article, 6 | }; 7 | 8 | #[must_use] 9 | #[component] 10 | pub fn EstaSemanaEnRust(article: Article) -> impl IntoView { 11 | let title = article.title.clone(); 12 | 13 | let author = if let Some(github_user) = article.github_user.clone() { 14 | github_user 15 | } else { 16 | article.author.clone().unwrap_or_default() 17 | }; 18 | 19 | let description = format!("{} - By @{}", article.description, author); 20 | view! { 21 | <> 22 | 23 | 24 | 25 | 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A Blog"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | rust-overlay, 13 | ... 14 | }: let 15 | system = "x86_64-linux"; 16 | overlays = [(import rust-overlay)]; 17 | #pkgs = nixpkgs.legacyPackages.${system}; 18 | pkgs = import nixpkgs { 19 | inherit system overlays; 20 | }; 21 | rust = pkgs.buildPackages.rust-bin.stable.latest.minimal; 22 | buildInputs = with pkgs; [ 23 | rust 24 | leptosfmt 25 | cargo-watch 26 | miniserve 27 | openssl 28 | tailwindcss 29 | pkg-config 30 | ]; 31 | in { 32 | formatter.x86_64-linux = nixpkgs.legacyPackages.${system}.alejandra; 33 | environment.variables = { 34 | PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; 35 | }; 36 | devShells.${system}.default = pkgs.mkShell { 37 | inherit buildInputs; 38 | shellHook = ''mkdir -p out 39 | echo "Bienvenido al Blog." 40 | echo -e 'puede usar los comandos: 41 | \x1b[93m#[Para compilar y ejecutar el servidor web local]\x1b[0m 42 | cargo watch -x run --shell "tailwindcss -i ./input.css -o ./out/output.css && cargo run" & 43 | \x1b[93m#[Para ejecutar o correr los archivos estáticos de tu sitio web localmente]\x1b[0m 44 | miniserve out --index index.html' 45 | ''; 46 | }; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, fs}; 2 | 3 | use rss::{validation::Validate, Category, ChannelBuilder, Item}; 4 | 5 | use crate::models::article::Article; 6 | 7 | /// # Panics 8 | /// This can panic if `validate` fails or if `fs::write` fails. 9 | pub fn generate_feed_rss( 10 | articles: &[Article], 11 | out_file: &str, 12 | title: &str, 13 | description: &str, 14 | link_path: Option<&str>, 15 | ) { 16 | let categories = articles 17 | .iter() 18 | .flat_map(|a| a.tags.clone().unwrap_or_default()) 19 | .collect::>(); 20 | 21 | let categories = categories 22 | .iter() 23 | .map(|c| Category { 24 | name: c.to_string(), 25 | domain: None, 26 | }) 27 | .collect::>(); 28 | 29 | let items = articles 30 | .get(..4) 31 | .unwrap_or_default() 32 | .iter() 33 | .map(Into::into) 34 | .collect::>(); 35 | 36 | let channel = ChannelBuilder::default() 37 | .language(Some("es".to_string())) 38 | .title(title.to_string()) 39 | .description(description.to_string()) 40 | .link(format!( 41 | "https://blog.rustlang-es.org/{}", 42 | link_path.unwrap_or_default() 43 | )) 44 | .categories(categories) 45 | .items(items) 46 | .build(); 47 | 48 | channel.validate().unwrap(); 49 | 50 | let channel_str = channel.to_string(); 51 | 52 | fs::write(out_file, channel_str).unwrap(); 53 | println!("wrote {out_file}"); 54 | } 55 | -------------------------------------------------------------------------------- /articles/bienvenido.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bienvenidos a Rust Lang en Español 3 | description: Este es el primer post de la comunidad de Rust en Español. En este blog publicaremos noticias, tutoriales, guías, y todo lo que se nos ocurra sobre Rust. 4 | author: RustLangES 5 | github_user: RustLangES 6 | date: 2023-09-17 7 | tags: 8 | - rust 9 | - comunidad 10 | - "anuncio de la comunidad" 11 | social: 12 | github: https://github.com/RustLangES 13 | website: https://www.rustlang-es.org 14 | --- 15 | 16 | Este es el primer post de la comunidad de Rust en Español. En este blog publicaremos noticias, tutoriales, guías, y todo lo que se nos ocurra sobre Rust. 17 | 18 | 19 | 20 | 21 | ## ¿Qué es Rust? 22 | 23 | Rust es un lenguaje de programación de sistemas que se enfoca en la seguridad, 24 | la velocidad y la concurrencia. Su sintaxis es similar a la de C++, pero 25 | introduce conceptos nuevos que lo hacen más seguro y más fácil de usar. 26 | 27 | 28 | ## ¿Por qué Rust? 29 | 30 | Rust es un lenguaje de programación relativamente nuevo, pero que está ganando 31 | mucha popularidad. Es un lenguaje de programación de sistemas, lo que significa 32 | que es un lenguaje que se puede usar para crear programas que interactúan con 33 | el hardware de la computadora, como los sistemas operativos, los navegadores, 34 | los videojuegos, etc. 35 | 36 | Rust es un lenguaje de programación de sistemas que se enfoca en la seguridad, 37 | la velocidad y la concurrencia. Su sintaxis es similar a la de C++, pero 38 | introduce conceptos nuevos que lo hacen más seguro y más fácil de usar. 39 | 40 | Dejamos este video de referencia: 41 | 42 |
43 | 44 |
-------------------------------------------------------------------------------- /preview_generator/src/utils.rs: -------------------------------------------------------------------------------- 1 | use image::{ImageBuffer, Pixel, Rgba, RgbaImage}; 2 | 3 | pub fn chunked_string(s: String, words: usize, max_lines: usize) -> Vec { 4 | s.split_whitespace() 5 | .into_iter() 6 | .map(|s| s.to_string()) 7 | .collect::>() 8 | .chunks(words) 9 | .take(max_lines) 10 | .enumerate() 11 | .map(|(i, s)| { 12 | let s = s.join(" "); 13 | if i == max_lines - 1 { 14 | format!("{s}...") 15 | } else { 16 | s 17 | } 18 | }) 19 | .collect::>() 20 | } 21 | 22 | pub fn append_image(img: &mut RgbaImage, other: &RgbaImage, x_min: u32, y_min: u32, new_alpha: u8) { 23 | let mut logo_x = 0; 24 | for x in x_min..x_min + other.width() { 25 | let mut logo_y = 0; 26 | for y in y_min..y_min + other.height() { 27 | let pixel = other.get_pixel(logo_x, logo_y); 28 | logo_y += 1; 29 | if pixel.0[3] == 0 { 30 | continue; 31 | } 32 | let old_pixel = img.get_pixel_mut(x, y); 33 | old_pixel.blend(&pixel.map_with_alpha(|c| c, |_| new_alpha)); 34 | } 35 | logo_x += 1; 36 | } 37 | } 38 | 39 | #[allow(unused)] 40 | pub fn vertical_gradient( 41 | img: &mut RgbaImage, 42 | (x, y): (i64, i64), 43 | (width, height): (u32, u32), 44 | start: &Rgba, 45 | stop: &Rgba, 46 | ) 47 | { 48 | let mut gradient_img = ImageBuffer::new(width, height); 49 | image::imageops::vertical_gradient(&mut gradient_img, start, stop); 50 | 51 | image::imageops::overlay(img, &mut gradient_img, x, y); 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/article_page.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | use crate::{ 4 | components::{blog_content::BlogContent, layout::Layout}, 5 | models::article::Article, 6 | }; 7 | 8 | #[must_use] 9 | #[component] 10 | pub fn ArticlePage(article: Article) -> impl IntoView { 11 | let title = article.title.clone(); 12 | 13 | let author = if let Some(github_user) = article.github_user.clone() { 14 | github_user 15 | } else { 16 | article.author.clone().unwrap_or_default() 17 | }; 18 | 19 | let description = format!("{} - By @{}", article.description, author); 20 | view! { 21 | <> 22 | 27 | 28 | 29 |
30 | 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Cloudflare Pages 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - ".github/workflows/**.yml" 9 | - ".rusty-hook.toml" 10 | - "**/Makefile.toml" 11 | - "**.py" 12 | - "README.md" 13 | workflow_run: 14 | workflows: ["Generate This Week in Rust"] 15 | types: 16 | - completed 17 | 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | with: 31 | submodules: "recursive" 32 | - uses: dtolnay/rust-toolchain@stable 33 | 34 | - uses: Swatinem/rust-cache@v2 35 | - name: Build 36 | run: npm i && npx tailwindcss -i ./input.css -o ./out/blog/output.css && RUST_BACKTRACE=1 cargo run --release 37 | - name: Generate Previews 38 | run: | 39 | cd preview_generator 40 | cargo run --release -- blog ../articles ../out/blog/articles 41 | cargo run --release -- this_week ../esta_semana_en_rust/ ../out/blog/articles 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-artifact@v4 45 | with: 46 | path: ./out/blog 47 | 48 | deploy: 49 | runs-on: ubuntu-latest 50 | needs: build 51 | steps: 52 | - uses: actions/download-artifact@v4 53 | - name: Deploy 54 | uses: cloudflare/wrangler-action@v3 55 | with: 56 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 57 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 58 | command: pages deploy ./artifact --project-name=blog 59 | -------------------------------------------------------------------------------- /preview_generator/src/components/tags.rs: -------------------------------------------------------------------------------- 1 | use image::{RgbaImage, Rgba}; 2 | use imageproc::drawing::text_size; 3 | use rusttype::{Font, Scale}; 4 | 5 | use super::circle_rect; 6 | use super::rounded_rect::rounded_rect; 7 | 8 | pub fn rounded_tag( 9 | img: &mut RgbaImage, 10 | font: &Font, 11 | font_size: f32, 12 | bg_color: Rgba, 13 | text_color: Rgba, 14 | radius: i32, 15 | pos: (i32, i32), 16 | padding: (i32, i32), 17 | content: String, 18 | ) -> (i32, i32) { 19 | let (text_width, text_height) = text_size(Scale::uniform(font_size), font, &content); 20 | let size = ((padding.0 * 2) + text_width, (padding.1 * 2) + text_height); 21 | 22 | rounded_rect(img, bg_color, radius, pos, size); 23 | 24 | imageproc::drawing::draw_text_mut( 25 | img, 26 | text_color, 27 | padding.0 + pos.0, 28 | padding.1 + pos.1, 29 | Scale::uniform(font_size), 30 | font, 31 | &content, 32 | ); 33 | 34 | size 35 | } 36 | 37 | pub fn circle_tag( 38 | img: &mut RgbaImage, 39 | font: &Font, 40 | font_size: f32, 41 | bg_color: Rgba, 42 | text_color: Rgba, 43 | pos: (i32, i32), 44 | padding: (i32, i32), 45 | content: String, 46 | ) -> (i32, i32) { 47 | let (text_width, text_height) = text_size(Scale::uniform(font_size), font, &content); 48 | let size = ((padding.0 * 2) + text_width, (padding.1 * 2) + text_height); 49 | 50 | circle_rect(img, bg_color, pos, size); 51 | 52 | imageproc::drawing::draw_text_mut( 53 | img, 54 | text_color, 55 | padding.0 + pos.0, 56 | padding.1 + pos.1, 57 | Scale::uniform(font_size), 58 | font, 59 | &content, 60 | ); 61 | 62 | size 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/components/icons/mod.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | use comments::CommentIcon; 4 | use github::GithubIcon; 5 | use linkedin::LinkedinIcon; 6 | use logo_rust_page::LogoRustPageIcon; 7 | use next::NextIcon; 8 | use twitter::TwitterIcon; 9 | use website::WebsiteIcon; 10 | 11 | pub mod comments; 12 | pub mod github; 13 | pub mod linkedin; 14 | pub mod logo_rust_page; 15 | pub mod next; 16 | pub mod twitter; 17 | pub mod website; 18 | 19 | #[component] 20 | #[must_use] 21 | #[allow(clippy::needless_pass_by_value)] 22 | pub fn StrToIcon( 23 | #[prop(into)] v: String, 24 | #[prop(default = 40)] size: u32, 25 | #[prop(default = "")] class: &'static str, 26 | ) -> impl IntoView { 27 | let class = "fill-black dark:fill-white ".to_owned() + class; 28 | 29 | match v.as_str() { 30 | "github" => view! { 31 | <> 32 | 33 | 34 | }, 35 | "twitter" => view! { 36 | <> 37 | 38 | 39 | }, 40 | "website" => view! { 41 | <> 42 | 43 | 44 | }, 45 | "next" => view! { 46 | <> 47 | 48 | 49 | }, 50 | "linkedin" => view! { 51 | <> 52 | 53 | 54 | }, 55 | "comment" => view! { 56 | <> 57 | 58 | 59 | }, 60 | "rust" => view! { 61 | <> 62 | 63 | 64 | }, 65 | _ => view! { <> }, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/button_link.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, Children, IntoView}; 2 | use std::collections::HashMap; 3 | 4 | #[component] 5 | #[must_use] 6 | pub fn ButtonLink( 7 | href: &'static str, 8 | #[prop(default = "primary")] color: &'static str, 9 | #[prop(default = "normal")] size: &'static str, 10 | #[prop(default = "drop")] shadow: &'static str, 11 | #[prop(into, optional)] class: &'static str, 12 | children: Children, 13 | ) -> impl IntoView { 14 | let colors = HashMap::from([ 15 | ( 16 | "primary", 17 | "bg-orange-200 dark:bg-transparent hover:bg-black hover:text-white", 18 | ), 19 | ("white", "bg-orange-100 dark:bg-transparent hover:bg-black"), 20 | ]); 21 | let shadows = HashMap::from([ 22 | ("drop", "drop-shadow-[4px_4px_0_rgba(0,0,0)] hover:drop-shadow-[0_0_0_rgba(0,0,0)]"), 23 | ("box", "drop-shadow-[4px_4px_0_rgba(0,0,0)] hover:drop-shadow-[0_0_0_rgba(0,0,0)] dark:drop-shadow-none shadow-sm hover:drop-shadow-none dark:hover:shadow-lg shadow-black"), 24 | ]); 25 | let sizes = HashMap::from([("tiny", "min-h-7"), ("normal", "h-9"), ("big", "h-12")]); 26 | let current_color = (*colors.get(&color).unwrap()).to_string(); 27 | let current_size = (*sizes.get(&size).unwrap()).to_string(); 28 | let shadow = (*shadows.get(&shadow).unwrap()).to_string(); 29 | 30 | view! { 31 | 42 | 43 | {children()} 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/async_component.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use futures::Future; 4 | use leptos::{ 5 | component, create_resource, IntoView, Resource, Serializable, SerializationError, SignalGet, 6 | Suspense, SuspenseProps, View, ViewFn, 7 | }; 8 | 9 | #[component] 10 | /// Async wraps an async function that returns an IntoView into a Suspense. 11 | /// 12 | /// This is useful when used together with async ssr that will wait on the provided async function 13 | /// to render the final view. 14 | pub fn Async(view: F) -> impl IntoView 15 | where 16 | V: IntoView + 'static, 17 | Fut: Future + 'static, 18 | F: Fn() -> Fut + 'static, 19 | { 20 | // create an async resource that will return the view. It is wrapped in a Wrapper so that it 21 | // implements the Serializable trait. This works because it will only be rendered on the server 22 | // and never serialized. 23 | let once: Resource<(), Wrapper> = 24 | create_resource(|| (), move |()| Wrapper::wrap_view(view())); 25 | 26 | Suspense(SuspenseProps { 27 | fallback: ViewFn::default(), 28 | children: Rc::new(move || once.get()), 29 | }) 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | /// Wrapper makes something implement the Serializable trait, any tries to serialize or deserialize 34 | /// this will panic. 35 | struct Wrapper(T); 36 | 37 | impl Wrapper { 38 | pub async fn wrap_view>(f: Fut) -> Wrapper { 39 | Wrapper(f.await.into_view()) 40 | } 41 | } 42 | 43 | impl Serializable for Wrapper { 44 | fn ser(&self) -> Result { 45 | panic!("this should never be called"); 46 | } 47 | 48 | fn de(_bytes: &str) -> Result { 49 | panic!("this should never be called"); 50 | } 51 | } 52 | 53 | impl IntoView for Wrapper 54 | where 55 | T: IntoView, 56 | { 57 | fn into_view(self) -> View { 58 | self.0.into_view() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use futures::{Stream, StreamExt}; 2 | use leptos::{ 3 | provide_context, ssr::render_to_stream_in_order_with_prefix_undisposed_with_context, View, 4 | }; 5 | use lol_html::{element, HtmlRewriter, Settings}; 6 | use std::{error::Error, pin::Pin}; 7 | use tokio::task; 8 | 9 | use crate::meta; 10 | 11 | /// # Panics 12 | /// This can panic if `clean_leptos_ssr` fails during execution. 13 | pub async fn render( 14 | view: impl FnOnce() -> View + 'static, 15 | additional_context: impl FnOnce() + 'static, 16 | ) -> String { 17 | let local = task::LocalSet::new(); 18 | local 19 | .run_until(async move { 20 | let shell_ctx = meta::ShellCtx::new(); 21 | let shell_ctx2 = shell_ctx.clone(); 22 | 23 | let (stream, runtime) = render_to_stream_in_order_with_prefix_undisposed_with_context( 24 | view, 25 | || "".into(), 26 | || { 27 | provide_context(shell_ctx); 28 | additional_context(); 29 | }, 30 | ); 31 | let stream = Box::pin(stream); 32 | let out = clean_leptos_ssr(stream).await.unwrap(); 33 | runtime.dispose(); 34 | 35 | shell_ctx2.render(&out) 36 | }) 37 | .await 38 | } 39 | 40 | async fn clean_leptos_ssr( 41 | mut stream: Pin>>, 42 | ) -> Result> { 43 | let mut output = vec![]; 44 | 45 | let mut rewriter = HtmlRewriter::new( 46 | Settings { 47 | element_content_handlers: vec![element!("script:nth-of-type(1)", |el| { 48 | el.remove(); 49 | Ok(()) 50 | })], 51 | ..Settings::default() 52 | }, 53 | |c: &[u8]| output.extend_from_slice(c), 54 | ); 55 | 56 | while let Some(chunk) = stream.next().await { 57 | rewriter.write(chunk.as_bytes())?; 58 | } 59 | rewriter.end()?; 60 | 61 | Ok(String::from_utf8(output)?) 62 | } 63 | -------------------------------------------------------------------------------- /preview_generator/src/components/rounded_rect.rs: -------------------------------------------------------------------------------- 1 | use image::{RgbaImage, Rgba}; 2 | use imageproc::drawing::{draw_filled_ellipse_mut, draw_filled_rect_mut}; 3 | use imageproc::rect::Rect; 4 | 5 | pub fn rounded_rect( 6 | img: &mut RgbaImage, 7 | color: Rgba, 8 | radius: i32, 9 | (x, y): (i32, i32), 10 | (w, h): (i32, i32), 11 | ) { 12 | let half_rad = radius / 2; 13 | // let mut img = img 14 | // .view(pos.0 as u32, pos.1 as u32, size.0 as u32, size.1 as u32) 15 | // .to_image(); 16 | // Top Left 17 | draw_filled_ellipse_mut(img, (x + radius, y + half_rad), radius, radius, color); 18 | // Top Right 19 | draw_filled_ellipse_mut(img, (x + w - half_rad, y + half_rad), radius, radius, color); 20 | // Bottom Left 21 | draw_filled_ellipse_mut(img, (x + radius, y + h - half_rad), radius, radius, color); 22 | // Bottom Right 23 | draw_filled_ellipse_mut( 24 | img, 25 | (x + w - half_rad, y + h - half_rad), 26 | radius, 27 | radius, 28 | color, 29 | ); 30 | 31 | // main box 32 | draw_filled_rect_mut( 33 | img, 34 | Rect::at(x + radius, y + radius) 35 | .of_size(w as u32 - radius as u32, h as u32 - radius as u32), 36 | color, 37 | ); 38 | 39 | // top box 40 | draw_filled_rect_mut( 41 | img, 42 | Rect::at(x + radius, y - half_rad).of_size(w as u32 - radius as u32, radius as u32 * 2), 43 | color, 44 | ); 45 | // left box 46 | draw_filled_rect_mut( 47 | img, 48 | Rect::at(x, y + half_rad).of_size(radius as u32, h as u32 - half_rad as u32 * 2), 49 | color, 50 | ); 51 | // right box 52 | draw_filled_rect_mut( 53 | img, 54 | Rect::at(x + w - half_rad + 2, y + half_rad) 55 | .of_size(radius as u32, h as u32 - half_rad as u32 * 2), 56 | color, 57 | ); 58 | // bottom box 59 | draw_filled_rect_mut( 60 | img, 61 | Rect::at(x + radius, y + h - radius - 3) 62 | .of_size(w as u32 - radius as u32, radius as u32 * 2), 63 | color, 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/pagination_buttons.rs: -------------------------------------------------------------------------------- 1 | use crate::components::icons::StrToIcon; 2 | use leptos::{component, view, IntoView, Show}; 3 | 4 | #[component] 5 | #[must_use] 6 | pub fn PaginationButtons( 7 | hide: bool, 8 | current_page: Option, 9 | max_page: usize, 10 | ) -> impl IntoView { 11 | let page_number = current_page.unwrap_or(0); 12 | 13 | let show_next_page_button = page_number < max_page || max_page == 0; 14 | let show_prev_page_button = page_number > 0; 15 | 16 | view! { 17 | <> 18 | 19 | {if hide { 20 | view! { <> } 21 | } else { 22 | view! { 23 | <> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | } 32 | }} 33 | 34 | } 35 | } 36 | 37 | #[component] 38 | #[must_use] 39 | pub fn PreviousPageButton(page: Option) -> impl IntoView { 40 | let page = page.unwrap_or(0); 41 | 42 | let previous_page = if page == 1 { 43 | "/".to_string() 44 | } else { 45 | format!("/pages/{}.html", page - 1) 46 | }; 47 | 48 | view! { 49 | <> 50 | 54 | 55 | "Pagina anterior" 56 | 57 | 58 | } 59 | } 60 | 61 | #[component] 62 | #[must_use] 63 | pub fn NextPageButton(page: Option) -> impl IntoView { 64 | let page = page.unwrap_or(0); 65 | let link = format!("/pages/{}.html", page + 1); 66 | 67 | view! { 68 | <> 69 | 73 | "Siguiente pagina" 74 | 75 | 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /preview_generator/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | use std::fs; 3 | 4 | use gray_matter::engine::YAML; 5 | use gray_matter::Matter; 6 | use image::RgbaImage; 7 | use models::Article; 8 | use rusttype::Font; 9 | 10 | mod blog; 11 | mod components; 12 | mod models; 13 | mod this_week; 14 | mod utils; 15 | 16 | pub const WIDTH: u32 = 1200; 17 | pub const HEIGHT: u32 = 630; 18 | 19 | const REGULAR_FONT_BYTES: &[u8] = include_bytes!("../assets/WorkSans-Regular.ttf"); 20 | const BOLD_FONT_BYTES: &[u8] = include_bytes!("../assets/WorkSans-Bold.ttf"); 21 | 22 | pub trait PreviewGenerator { 23 | fn gen( 24 | &self, 25 | img: &mut RgbaImage, 26 | file_name: String, 27 | font: &Font, 28 | bold: &Font, 29 | article: Article, 30 | output: &str, 31 | ); 32 | } 33 | 34 | fn main() { 35 | let mut argv = args().skip(1); 36 | let Some(file_type) = argv.next() else { 37 | panic!("No hay el typo de arhivo en los argumentos"); 38 | }; 39 | let Some(folder) = argv.next() else { 40 | panic!("No hay la carpeta en los argumentos"); 41 | }; 42 | let Some(output) = argv.next() else { 43 | panic!("No hay la carpeta de salida en los argumentos"); 44 | }; 45 | 46 | match file_type.as_str() { 47 | "blog" => generate(folder, output, blog::BlogGenerator::default()), 48 | "this_week" => generate(folder, output, this_week::ThisWeekGenerator::default()), 49 | x => panic!("El tipo de archivo '{x}' no es admitido"), 50 | }; 51 | } 52 | 53 | pub fn generate(folder: String, output: String, generator: G) { 54 | let folder_content = fs::read_dir(folder).unwrap(); 55 | let font = Font::try_from_bytes(REGULAR_FONT_BYTES).unwrap(); 56 | let bold = Font::try_from_bytes(BOLD_FONT_BYTES).unwrap(); 57 | 58 | for post_file in folder_content { 59 | let post_file = post_file.unwrap(); 60 | let file_name = post_file.file_name().to_str().unwrap().to_string(); 61 | let file = post_file.path(); 62 | let content = fs::read_to_string(file).unwrap(); 63 | let matter = Matter::::new(); 64 | let Some(article) = matter.parse_with_struct::
(&content) else { 65 | continue; 66 | }; 67 | 68 | let mut img = RgbaImage::new(WIDTH, HEIGHT); 69 | generator.gen( 70 | &mut img, 71 | file_name.replace(".md", ""), 72 | &font, 73 | &bold, 74 | article.data, 75 | &output, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/models/devto_article.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | pub type DevToArticles = Vec; 5 | 6 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct DevToArticle { 9 | #[serde(rename = "type_of")] 10 | pub type_of: String, 11 | pub id: u32, 12 | pub title: String, 13 | pub description: String, 14 | #[serde(rename = "readable_publish_date")] 15 | pub readable_publish_date: String, 16 | pub slug: String, 17 | pub path: String, 18 | pub url: String, 19 | #[serde(rename = "comments_count")] 20 | pub comments_count: i64, 21 | #[serde(rename = "public_reactions_count")] 22 | pub public_reactions_count: i64, 23 | #[serde(rename = "collection_id")] 24 | pub collection_id: Value, 25 | #[serde(rename = "published_timestamp")] 26 | pub published_timestamp: String, 27 | #[serde(rename = "positive_reactions_count")] 28 | pub positive_reactions_count: i64, 29 | #[serde(rename = "cover_image")] 30 | pub cover_image: Value, 31 | #[serde(rename = "social_image")] 32 | pub social_image: String, 33 | #[serde(rename = "canonical_url")] 34 | pub canonical_url: String, 35 | #[serde(rename = "created_at")] 36 | pub created_at: String, 37 | #[serde(rename = "edited_at")] 38 | pub edited_at: Value, 39 | #[serde(rename = "crossposted_at")] 40 | pub crossposted_at: Value, 41 | #[serde(rename = "published_at")] 42 | pub published_at: String, 43 | #[serde(rename = "last_comment_at")] 44 | pub last_comment_at: String, 45 | #[serde(rename = "reading_time_minutes")] 46 | pub reading_time_minutes: i64, 47 | #[serde(rename = "tag_list")] 48 | pub tag_list: Vec, 49 | pub tags: String, 50 | pub user: User, 51 | #[serde(skip_deserializing)] 52 | pub content: Option, 53 | #[serde(skip_deserializing)] 54 | pub content_html: Option, 55 | } 56 | 57 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 58 | #[serde(rename_all = "camelCase")] 59 | pub struct User { 60 | pub name: String, 61 | pub username: String, 62 | #[serde(rename = "twitter_username")] 63 | pub twitter_username: String, 64 | #[serde(rename = "github_username")] 65 | pub github_username: String, 66 | #[serde(rename = "user_id")] 67 | pub user_id: i64, 68 | #[serde(rename = "website_url")] 69 | pub website_url: String, 70 | #[serde(rename = "profile_image")] 71 | pub profile_image: String, 72 | #[serde(rename = "profile_image_90")] 73 | pub profile_image_90: String, 74 | } 75 | -------------------------------------------------------------------------------- /articles/fundamentals-ownership.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fundamentals | Ownership 3 | description: El Ownership es una característica fundamental del lenguaje que se utiliza para gestionar la asignación y liberación de memoria de manera segura y prevenir errores. 4 | author: Michael Cardoza 5 | github_user: michaelcardoza 6 | date: 2023-09-18 7 | tags: 8 | - rust 9 | - comunidad 10 | social: 11 | github: https://github.com/RustLangES 12 | website: https://www.rustlang-es.org 13 | --- 14 | 15 | El __Ownership__ es una característica fundamental del lenguaje que se utiliza para gestionar la asignación y liberación de memoria de manera segura y prevenir errores. 16 | 17 | 18 | 19 | 20 | ## ¿Qué es el ownership en Rust? 21 | 22 | Es fundamental para los programadores rastrear la memoria, ya que no hacerlo puede resultar en una "fuga de memoria" - (Leak). Rust utiliza un modelo de "propiedad" conocido como Ownership para gestionar la memoria. 23 | 24 | El __Ownership__ (propiedad) en Rust nos permite tener un control extremadamente preciso sobre la gestión de la memoria, garantizando la seguridad y la prevención de errores de acceso a la memoria en tiempo de ejecución. 25 | 26 | 27 | ## ¿Comó funciona el ownership en Rust? 28 | 29 | En Rust, la memoria se puede "mover" (move) o "tomar prestada" (borrowed). 30 | 31 | ### Ejemplo: Move 32 | 33 | ```rust 34 | enum Light { 35 | Bright, 36 | Dull, 37 | } 38 | 39 | fn display_light(light: Light) { 40 | match light { 41 | Light::Bright => println!("bright"), 42 | Light::Dull => println!("dull"), 43 | } 44 | } 45 | 46 | fn main() { 47 | let dull = Light::Dull; 48 | 49 | display_light(dull); 50 | display_light(dull); // will not work 51 | } 52 | ``` 53 | 54 | En este ejemplo, el valor asignado a la variable `dull` se "moverá" al ámbito de la primera función `display_light` que se ejecuta. Como resultado, ya no está disponible en el ámbito de la función main. Cuando se intenta ejecutar la segunda función `display_light`, el valor de la variable `dull` ya no está disponible. 55 | 56 | ### Ejemplo: Borrow 57 | 58 | ```rust 59 | enum Light { 60 | Bright, 61 | Dull, 62 | } 63 | 64 | fn display_light(light: &Light) { 65 | match light { 66 | Light::Bright => println!("bright"), 67 | Light::Dull => println!("dull"), 68 | } 69 | } 70 | 71 | fn main() { 72 | let dull = Light::Dull; 73 | 74 | display_light(&dull); 75 | display_light(&dull); 76 | } 77 | ``` 78 | 79 | En este segundo ejemplo, agregamos el símbolo `&`. 80 | 81 | El símbolo `&` indica una referencia (puntero) a una instancia de Light. En otras palabras, estamos "prestando" el valor de `dull` a la ejecución de cada función `display_light`, en lugar de moverlo. Esto permite que el valor de dull siga estando disponible después de llamar a la función `display_light`. 82 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1705309234, 9 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1711163522, 24 | "narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "44d0940ea560dee511026a53f0e2e2cde489b4d4", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1706487304, 40 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "nixpkgs": "nixpkgs", 56 | "rust-overlay": "rust-overlay" 57 | } 58 | }, 59 | "rust-overlay": { 60 | "inputs": { 61 | "flake-utils": "flake-utils", 62 | "nixpkgs": "nixpkgs_2" 63 | }, 64 | "locked": { 65 | "lastModified": 1711246447, 66 | "narHash": "sha256-g9TOluObcOEKewFo2fR4cn51Y/jSKhRRo4QZckHLop0=", 67 | "owner": "oxalica", 68 | "repo": "rust-overlay", 69 | "rev": "dcc802a6ec4e9cc6a1c8c393327f0c42666f22e4", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "oxalica", 74 | "repo": "rust-overlay", 75 | "type": "github" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /src/components/icons/logo_rust_page.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | 3 | #[component] 4 | #[must_use] 5 | pub fn LogoRustPageIcon( 6 | #[prop(default = 40)] size: u32, 7 | #[prop(into, default = "dark:fill-[#e2cea9]".to_string())] class: String, 8 | ) -> impl IntoView { 9 | view! { 10 | 17 | 18 | 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gen_translated.py: -------------------------------------------------------------------------------- 1 | import os, requests, uuid 2 | import re 3 | 4 | key_var_name = 'API_KEY' 5 | if not key_var_name in os.environ: 6 | raise Exception('Please set/export the environment variable: {}'.format(key_var_name)) 7 | resource_key = os.environ[key_var_name] 8 | 9 | region_var_name = 'REGION' 10 | if not region_var_name in os.environ: 11 | raise Exception('Please set/export the environment variable: {}'.format(region_var_name)) 12 | region = os.environ[region_var_name] 13 | 14 | endpoint_var_name = 'API_ENDPOINT' 15 | if not endpoint_var_name in os.environ: 16 | raise Exception('Please set/export the environment variable: {}'.format(endpoint_var_name)) 17 | endpoint = os.environ[endpoint_var_name] 18 | 19 | path = '/translate?api-version=3.0' 20 | params = '&from=en&to=es' 21 | constructed_url = endpoint + path + params 22 | 23 | raw_date = 'DATE' 24 | if not raw_date in os.environ: 25 | raise Exception('Please set/export the environment variable: {}'.format(raw_date)) 26 | raw_date = os.environ[raw_date] 27 | 28 | if not os.path.isfile(raw_date + ".md"): 29 | raise Exception('The file not exists: {}'.format(raw_date)) 30 | raw_file = open(raw_date + ".md", 'r') 31 | 32 | headers = { 33 | 'Ocp-Apim-Subscription-Key': resource_key, 34 | 'Ocp-Apim-Subscription-Region': region, 35 | 'Content-type': 'application/json', 36 | 'X-ClientTraceId': str(uuid.uuid4()) 37 | } 38 | 39 | # You can pass more than one object in body. 40 | original_text = raw_file.read() 41 | 42 | body = [{ 43 | 'text': original_text 44 | }] 45 | request = requests.post(constructed_url, headers=headers, json=body) 46 | response = request.json() 47 | 48 | raw_file.close() 49 | 50 | # generate ouput 51 | meta_content = open(raw_date + "-this-week-in-rust.md", "r").read() 52 | with open(raw_date + "-this-week-in-rust.md", 'w') as fh: 53 | print(response) 54 | content = "" 55 | if response and isinstance(response, list) and response[0] and "translations" in response[0] and response[0]["translations"]: 56 | content = response[0]["translations"][0]["text"] 57 | else: 58 | content = original_text 59 | print('No se pudo traducir') 60 | print("Response: ") 61 | print(response) 62 | 63 | description = [line for line in content.split('\n') if line.startswith("La caja de esta semana es")] 64 | print(f"Match = {description}") 65 | description = description[0] 66 | finded = re.search(r'(\[(?P.*?)\])\((?P.*?)(?P\".*?\")?\)', description) 67 | if finded is None: 68 | print("No Encontrado") 69 | description = "Esta semana en Rust es un blog semanal sobre el lenguaje de programación Rust, sus comunidades y su ecosistema." 70 | else: 71 | print("Encontrado!!") 72 | finded = finded.groupdict() 73 | link_name = re.sub(r'\[.*\]\(.*\)', finded["caption"], description) 74 | print(f"To Replace: {meta_content}") 75 | new_content = meta_content.replace("Esta semana en Rust es un blog semanal sobre el lenguaje de programación Rust, sus comunidades y su ecosistema.", link_name) 76 | print(f"Replacement Result: {new_content}") 77 | content = new_content + '\n' + content 78 | 79 | fh.write(content) 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![2024-05-25 06 10 19 blog rustlang-es org be8dd0ac9870](https://github.com/RustLangES/blog/assets/19656993/055ed112-e100-449d-bea6-e5110e7f483a) 2 | 3 |

4 | GitHub Workflow Status (with event) 5 | GitHub Workflow Status (with event) 6 |

7 | 8 | # 🤝🏼 Agrega tu articulo 9 | 10 | Pasos: 11 | 12 | - Haz fork de este proyecto 13 | - Crea un archivo Markdown en la carpeta `articles` 14 | - Escribe tu articulo con este formato 15 | 16 | ```md 17 | --- 18 | title: Mi Articulo 19 | description: La descripcion de mi articulo 20 | author: RustLangES 21 | github_user: RustLangES 22 | date: 2023-09-17 23 | tags: 24 | - rust 25 | - comunidad 26 | # Aqui compartes tus redes sociales 27 | social: 28 | github: https://github.com/RustLangES 29 | # twitter: 30 | # website: 31 | --- 32 | 33 | El Contenido de tu articulo 34 | ``` 35 | 36 | - Haz una PR con tus cambios 37 | - Espera nuestra revision 38 | - Disfruta de tu articulo publicado 🎊 39 | 40 | --- 41 | 42 | # Desarrollo 43 | 44 | ## Requisitos 45 | 46 | - [Rust](https://rust-lang.org/tools/install) 47 | - [NodeJs](https://nodejs.org) 48 | - [cargo-watch](https://crates.io/crates/cargo-watch) 49 | - [leptosfmt](https://crates.io/crates/leptosfmt) 50 | 51 | # Generar la web 52 | ``` 53 | - npm install 54 | 55 | # Instalar Linter 56 | - cargo install leptosfmt 57 | 58 | - cargo watch -x run --shell "npx tailwindcss -i ./input.css -o ./out/output.css && cargo run" 59 | 60 | # lanzar un servidor web provicional con python3 61 | - python3 -m http.server -d out 62 | 63 | # [alternativa] lanzar un servidor web rústico 😏 64 | - cargo install basic-http-server 65 | - basic-http-server out -a "0.0.0.0:8000" 66 | ``` 67 | 68 | # En cualquier linux distro 69 | ``` 70 | # Iniciar cargo watch y http server 71 | ./server start 72 | 73 | # Apagar ambos servicios 74 | ./server stop 75 | ``` 76 | 77 | # En cualquier windows 78 | ``` 79 | ## Iniciar cargo watch y http server 80 | ./server.bat start 81 | 82 | ## Apagar ambos servicios 83 | ./server.bat stop 84 | ``` 85 | 86 | ## Aclaraciones 87 | 88 | Si commiteas habra un githook que corra los linters. 89 | Es posible que encuentre errores de formato o mejoras que se pueden hacer. 90 | 91 | Para ver estos cambios puedes ejecutar 92 | 93 | ``` 94 | cargo clippy 95 | ``` 96 | 97 | Esto te mostrara algunos cambios que puedes hacer para mejorar el codigo. 98 | Cosas redudantes o que quizás no tengan sentido. 99 | 100 | ¡Hara tu código más idiomático! 101 | 102 | Otro en menor medida podría ser: 103 | 104 | ``` 105 | leptosfmt src 106 | ``` 107 | 108 | Este te formateara el código de forma automática. 109 | Puede llegar a romper algunas cosas de los componentes si se hizo un cambio allí. 110 | 111 | ## Agradecimientos 112 | Para la generacion de previews se utilizaron 113 | Etiqueta de precio icono de Icons8 114 | -------------------------------------------------------------------------------- /src/pages/home.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | async_component::Async, 3 | components::{ 4 | card_article::CardArticle, feature_articles::featured_articles, layout::Layout, 5 | pagination_buttons::PaginationButtons, 6 | }, 7 | models::article::Article, 8 | ARTICLES, 9 | }; 10 | use futures::executor::block_on; 11 | use leptos::{component, view, CollectView, IntoView}; 12 | 13 | async fn fetch_articles() -> Vec
{ 14 | ARTICLES.read().await.clone() 15 | } 16 | 17 | #[component] 18 | #[must_use] 19 | pub fn Homepage( 20 | articles: Option>, 21 | show_featured: bool, 22 | page: Option, 23 | max_page: usize, 24 | ) -> impl IntoView { 25 | let mut articles = articles.unwrap_or(block_on(fetch_articles())); 26 | 27 | if show_featured { 28 | articles = articles.into_iter().take(7).collect(); 29 | } 30 | let hide_pagination = max_page == 0 && !show_featured; 31 | 32 | view! { 33 | 34 |

35 | "Blog" 36 |

37 |

38 | "Revisa que esta pasando en la comunidad de Rust Lang en Español." 39 |

40 | {if show_featured { 41 | view! { } 42 | } else { 43 | view! { <> }.into_view() 44 | }} 45 | 46 |
47 |
48 |

49 | "Artículos" 50 |

51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 | } 59 | } 60 | 61 | #[component] 62 | fn grid_of_articles(articles: Vec
, is_home: bool) -> impl IntoView { 63 | let mut invalid_tags = vec![ 64 | "esta semana en rust".to_string(), 65 | "anuncio de la comunidad".to_string(), 66 | ]; 67 | 68 | let articles = if is_home { 69 | articles 70 | .into_iter() 71 | .filter(|article| filter_common_articles(article, &mut invalid_tags)) 72 | .collect::>() 73 | .into_iter() 74 | } else { 75 | articles.into_iter() 76 | }; 77 | 78 | view! { 79 |
80 | {articles.map(|article| CardArticle((article, is_home).into())).collect_view()} 81 |
82 | } 83 | } 84 | 85 | pub fn filter_common_articles(article: &Article, invalid_tags: &mut Vec) -> bool { 86 | if let Some(tags) = &article.tags { 87 | let invalid_tag = invalid_tags.iter().position(|tag| tags.contains(tag)); 88 | if let Some(invalid_tag) = invalid_tag { 89 | invalid_tags.remove(invalid_tag); 90 | return false; 91 | } 92 | true 93 | } else { 94 | true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/esta_semana_en_rust/blog_content.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | components::{ 3 | icons::StrToIcon, 4 | mdx::{ 5 | center::{Center, CenterProps}, 6 | youtube::{Youtube, YoutubeProps}, 7 | }, 8 | }, 9 | models::article::Article, 10 | }; 11 | use leptos::{component, view, IntoView}; 12 | use leptos_mdx::mdx::{Components, Mdx, MdxComponentProps}; 13 | 14 | #[component] 15 | pub fn BlogContent(#[prop()] article: Article) -> impl IntoView { 16 | let mut components = Components::new(); 17 | let social = article.social.clone().unwrap_or_default(); 18 | 19 | components.add_props( 20 | "youtube".to_string(), 21 | Youtube, 22 | |props: MdxComponentProps| { 23 | let video_id = props.attributes.get("video").unwrap().clone(); 24 | 25 | YoutubeProps { 26 | video: video_id.unwrap(), 27 | } 28 | }, 29 | ); 30 | 31 | components.add_props("center".to_string(), Center, |props: MdxComponentProps| { 32 | CenterProps { 33 | children: props.children, 34 | } 35 | }); 36 | 37 | view! { 38 |
39 |
40 |

41 | {article.title.clone()} 42 |

43 |
44 |
45 | {if article.has_author() { 46 | view! { 47 | <> 48 |
{article.author}
49 | 50 | } 51 | } else { 52 | view! { <> } 53 | }} 54 | {if social.is_empty() { 55 | view! { <> } 56 | } else { 57 | view! { 58 | <> 59 |
60 | 61 | } 62 | }} 63 |
64 | {social 65 | .iter() 66 | .map(|(net, url)| { 67 | view! { 68 | 69 | 70 | 71 | } 72 | }) 73 | .collect::>()} 74 |
75 |
76 | {article.date_string} 77 |
78 |
79 | 80 |
81 |
82 |
83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/meta.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, rc::Rc}; 2 | 3 | use leptos::{ 4 | component, expect_context, ssr::render_to_string, view, Children, CollectView, Fragment, 5 | IntoView, 6 | }; 7 | 8 | #[must_use] 9 | #[component] 10 | pub fn Html( 11 | #[prop(into)] mut attrs: Attrs, 12 | #[prop(optional, into, default = "")] class: &'static str, 13 | ) -> impl IntoView { 14 | let ctx = expect_context::(); 15 | let mut class = Attrs::from(vec![("class", class)]); 16 | ctx.body_attrs.borrow_mut().append(&mut class); 17 | ctx.html_attrs.borrow_mut().append(&mut attrs); 18 | } 19 | 20 | #[must_use] 21 | #[component] 22 | pub fn Head(children: Children) -> impl IntoView { 23 | let ctx = expect_context::(); 24 | ctx.head_els.borrow_mut().push(children()); 25 | } 26 | 27 | #[must_use] 28 | #[component(transparent)] 29 | pub fn Dedup(#[prop(into)] key: String, children: Children) -> impl IntoView { 30 | let ctx = expect_context::(); 31 | let mut map = ctx.deduped_head_els.borrow_mut(); 32 | map.entry(key).or_insert_with(children); 33 | } 34 | 35 | #[derive(Clone, Default)] 36 | /// `ShellCtx` holds all the elements that will be rendered to the of the page. 37 | /// It can be modified by any component by accessing the context, but it's suggested to be used in 38 | /// conjunction with the exported components , , <Html />, .... 39 | pub struct ShellCtx { 40 | head_els: Rc<RefCell<Vec<Fragment>>>, 41 | deduped_head_els: Rc<RefCell<HashMap<String, Fragment>>>, 42 | html_attrs: Rc<RefCell<Attrs>>, 43 | body_attrs: Rc<RefCell<Attrs>>, 44 | } 45 | 46 | impl ShellCtx { 47 | #[must_use] 48 | pub fn new() -> Self { 49 | Self::default() 50 | } 51 | 52 | #[must_use] 53 | pub fn render(self, inner_body: &str) -> String { 54 | let head = render_to_string(move || { 55 | view! { 56 | {self.head_els.borrow().clone().collect_view()} 57 | {self.deduped_head_els.borrow().values().collect_view()} 58 | } 59 | }); 60 | 61 | format!( 62 | "<!DOCTYPE html><html {}><head>{}</head><body {}>{}</body></html>", 63 | self.html_attrs.borrow().render(), 64 | head, 65 | self.body_attrs.borrow().render(), 66 | inner_body.trim(), 67 | ) 68 | } 69 | } 70 | 71 | /// Attrs is a list of attributes. 72 | #[derive(Default)] 73 | pub struct Attrs { 74 | pub attrs: Vec<(String, String)>, 75 | } 76 | 77 | impl Attrs { 78 | #[must_use] 79 | pub fn new() -> Self { 80 | Self { attrs: vec![] } 81 | } 82 | 83 | #[must_use] 84 | pub fn render(&self) -> String { 85 | self.attrs 86 | .iter() 87 | .map(|(k, v)| format!("{k}=\"{v}\"")) 88 | .collect::<Vec<_>>() 89 | .join(" ") 90 | } 91 | 92 | pub fn append(&mut self, other: &mut Self) { 93 | self.attrs.append(&mut other.attrs); 94 | } 95 | } 96 | 97 | impl From<Vec<(&str, &str)>> for Attrs { 98 | fn from(attrs: Vec<(&str, &str)>) -> Self { 99 | Self { 100 | attrs: attrs 101 | .iter() 102 | .map(|(k, v)| ((*k).to_string(), (*v).to_string())) 103 | .collect(), 104 | } 105 | } 106 | } 107 | 108 | impl From<Vec<(String, String)>> for Attrs { 109 | fn from(attrs: Vec<(String, String)>) -> Self { 110 | Self { attrs } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/components/header.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, create_signal, view, IntoView, SignalGet, SignalUpdate}; 2 | 3 | use crate::components::button_link::ButtonLink; 4 | use crate::components::icons::logo_rust_page::LogoRustPageIcon; 5 | 6 | #[component] 7 | #[must_use] 8 | pub fn Header() -> impl IntoView { 9 | let (is_open, set_is_open) = create_signal(false); 10 | 11 | view! { 12 | <header class="border-b border-b-black/20 bg-orange-200 dark:bg-transparent"> 13 | <div class="container mx-auto px-4 flex items-center justify-between flex-col lg:flex-row"> 14 | <div class="flex justify-between w-full lg:w-auto"> 15 | <a href="https://rustlang-es.org" exact=true class="flex items-center gap-x-4"> 16 | <LogoRustPageIcon size=80 /> 17 | </a> 18 | <button 19 | class="lg:hidden" 20 | on:click=move |_| { set_is_open.update(|n| *n = !*n) } 21 | aria-label="Menu de opciones" 22 | > 23 | <span class="w-6 h-1 bg-black block my-4 relative after:absolute after:block after:bg-black after:w-6 after:h-1 after:bottom-2 before:absolute before:block before:bg-black before:w-6 before:h-1 before:-bottom-2"></span> 24 | </button> 25 | </div> 26 | <nav class=move || { 27 | format!( 28 | "w-full lg:w-auto pb-10 pt-5 lg:p-0 {}", 29 | if is_open.get() { "block" } else { "hidden lg:block" }, 30 | ) 31 | }> 32 | 33 | <ul class="flex items-center gap-6 flex-col lg:flex-row lg:items-center"> 34 | <li class="nav-item"> 35 | <a href="https://book.rustlang-es.org" target="_blank"> 36 | "El libro" 37 | </a> 38 | </li> 39 | <li class="nav-item"> 40 | <a href="https://rustlang-es.org/aprende" target="_blank"> 41 | "Aprende" 42 | </a> 43 | </li> 44 | <li class="nav-item"> 45 | <a href="https://rustlang-es.org/comunidad">"Comunidad"</a> 46 | </li> 47 | <li class="nav-item"> 48 | <a href="https://rustlang-es.org/colaboradores">"Colaboradores"</a> 49 | </li> 50 | <li class="nav-item"> 51 | <a href="/">"Blog"</a> 52 | </li> 53 | <li> 54 | <ul class="lg:ml-4 flex items-center gap-x-6"> 55 | <li> 56 | <ButtonLink href="https://github.com/RustLangES"> 57 | "Github" 58 | </ButtonLink> 59 | </li> 60 | <li> 61 | <ButtonLink href="https://discord.gg/4ng5HgmaMg"> 62 | "Discord" 63 | </ButtonLink> 64 | </li> 65 | </ul> 66 | </li> 67 | </ul> 68 | </nav> 69 | </div> 70 | </header> 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/esta_semana_en_rust/header.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, create_signal, view, IntoView, SignalGet, SignalUpdate}; 2 | 3 | use crate::components::button_link::ButtonLink; 4 | 5 | #[must_use] 6 | #[component] 7 | pub fn Header() -> impl IntoView { 8 | let (is_open, set_is_open) = create_signal(false); 9 | 10 | view! { 11 | <header class="border-b border-b-black/20 bg-slate-900 text-white"> 12 | <div class="container mx-auto px-4 flex items-center justify-between flex-col lg:flex-row"> 13 | <div class="flex justify-between w-full lg:w-auto"> 14 | <a href="/" exact=true class="flex items-center gap-x-4"> 15 | <img 16 | src="https://www.rust-lang.org/static/images/rust-logo-blk.svg" 17 | class="max-h-20 rounded-full invert" 18 | height="80" 19 | width="80" 20 | alt="Rust Lang en Español" 21 | /> 22 | </a> 23 | <button 24 | class="lg:hidden" 25 | on:click=move |_| { set_is_open.update(|n| *n = !*n) } 26 | aria-label="Menu de opciones" 27 | > 28 | <span class="w-6 h-1 bg-black block my-4 relative after:absolute after:block after:bg-black after:w-6 after:h-1 after:bottom-2 before:absolute before:block before:bg-black before:w-6 before:h-1 before:-bottom-2"></span> 29 | </button> 30 | </div> 31 | <nav class=move || { 32 | format!( 33 | "w-full lg:w-auto pb-10 pt-5 lg:p-0 {}", 34 | if is_open.get() { "block" } else { "hidden lg:block" }, 35 | ) 36 | }> 37 | 38 | <ul class="flex items-center gap-6 flex-col lg:flex-row lg:items-center "> 39 | 40 | <li class="nav-item"> 41 | <a href="https://book.rustlang-es.org" target="_blank"> 42 | "El libro" 43 | </a> 44 | </li> 45 | <li class="nav-item"> 46 | <a href="/aprende" target="_blank"> 47 | "Aprende" 48 | </a> 49 | </li> 50 | <li class="nav-item"> 51 | <a href="/comunidad">"Comunidad"</a> 52 | </li> 53 | <li class="nav-item"> 54 | <a href="/colaboradores">"Colaboradores"</a> 55 | </li> 56 | <li class="nav-item"> 57 | <a href="/">"Blog"</a> 58 | </li> 59 | <li> 60 | <ul class="lg:ml-4 flex items-center gap-x-6 "> 61 | <li> 62 | <ButtonLink href="https://github.com/RustLangES"> 63 | "Github" 64 | </ButtonLink> 65 | </li> 66 | <li> 67 | <ButtonLink href="https://discord.gg/4ng5HgmaMg"> 68 | "Discord" 69 | </ButtonLink> 70 | </li> 71 | </ul> 72 | </li> 73 | </ul> 74 | </nav> 75 | </div> 76 | </header> 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /articles/cargo-generate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Crea tus proyectos con cargo generate 3 | description: Parametriza la creación de tus proyectos con cargo generate y liquid 4 | author: Jonathan D. 5 | github_user: jd-apprentice 6 | date: 2024-12-29 7 | tags: 8 | - rust 9 | - templates 10 | - cargo 11 | social: 12 | github: https://github.com/jd-apprentice 13 | website: https://jonathan.com.ar/ 14 | --- 15 | 16 | ## Que es cargo generate? 17 | 18 | Cargo generate es una herramienta que te permite crear proyectos basados en el sistema de templates `Liquid` el mismo es similar a Jinja2. 19 | 20 | El mismo utiliza un archivo llamado `cargo-generate.toml` en la raiz de tu proyecto para configurar el proyecto. 21 | 22 | Un ejemplo de como se veria 23 | 24 | ```toml 25 | [placeholders.package_name] 26 | type = "string" 27 | prompt = "What is the name of the package?" 28 | 29 | [placeholders.package_description] 30 | type = "string" 31 | prompt = "Enter a project description" 32 | ``` 33 | 34 | ## Instalación 35 | 36 | Para poder usarlo tendriamos que instalarlo, podemos hacerlo con nuestro manejador de paquetes en mi caso es `pacman -S cargo-generate`. 37 | 38 | Termina quedando accesible con `cargo generate` a nivel sistema 39 | 40 | ![cli](../assets/images/cargo-generate-cli.png) 41 | 42 | ## Como usarlo? 43 | 44 | Ahora si quiero instalar un template que lo tengo local podria hacer 45 | 46 | ```shell 47 | cargo generate --path jd-rust 48 | ``` 49 | 50 | Donde `path` es la ruta donde se encuentra el template 51 | 52 | Como tambien podemos usar un repositorio remoto 53 | 54 | ```shell 55 | cargo generate https://github.com/jd-apprentice/jd-rust 56 | ``` 57 | 58 | Ahora de forma interactivo es cuando nos va a preguntar para ir creando nuestro proyecto 59 | 60 | ![interactive](../assets/images/cargo-interactive.png) 61 | 62 | Cada uno de estos valores va a ser remplazado contra un archivo que tenga la extension `.liquid` o bien va a determinar la existencia de un archivo. 63 | 64 | Por ejemplo nuestro `rust-toolchain.toml.liquid` contiene lo siguiente 65 | 66 | ```toml 67 | [toolchain] 68 | channel = "{{ toolchain }}" 69 | profile = "default" 70 | components = ["clippy", "rustfmt"] 71 | ``` 72 | 73 | Entonces en esta pregunta 74 | 75 | ```shell 76 | ? 🤷 What is the rust toolchain version? › 77 | ❯ stable 78 | beta 79 | nightly 80 | ``` 81 | 82 | Va a remplazar lo que yo le diga con "{{ toolchain }}" esto se llama interpolación. 83 | 84 | ## Ejemplo completo 85 | 86 | ```shell 87 | cargo generate https://github.com/jd-apprentice/jd-rust 88 | ⚠️ Favorite `https://github.com/jd-apprentice/jd-rust` not found in config, using it as a git repository: https://github.com/jd-apprentice/jd-rust 89 | 🤷 Project Name: blog-rust 90 | 🔧 Destination: /home/dyallo/Documents/Proyectos/blog-rust ... 91 | 🔧 project-name: blog-rust ... 92 | 🔧 Generating template ... 93 | 🤷 What is the name of the package?: blog-rust 94 | 🤷 Enter a project description: Proyecto de prueba para el blog de rust 95 | ✔ 🤷 Do you want to include a MIT License? · false 96 | 🤷 What is your email? (CONTRIBUTING, LICENSE, etc): contacto@jonathan.com.ar 97 | 🤷 What is your github username? (CODEOWNERS): jd-apprentice 98 | ✔ 🤷 What is the rust toolchain version? · stable 99 | ✔ 🤷 What is the category of your package? · command-line-utilities 100 | ✔ 🤷 Do you want to create a github release? · false 101 | ✔ 🤷 Do you want to use sentry? · false 102 | 🔧 Moving generated files into: `/home/dyallo/Documents/Proyectos/blog-rust`... 103 | 🔧 Initializing a fresh Git repository 104 | ✨ Done! New project created /home/dyallo/Documents/Proyectos/blog-rust 105 | ``` 106 | 107 | Esto lo que hizo fue generar el siguiente proyecto 108 | 109 | ![example](../assets/images/blog-rust.png) 110 | 111 | ## Enlaces utilizados 112 | 113 | - [cargo-generate](https://github.com/cargo-generate/cargo-generate) 114 | - [jd-rust](https://github.com/jd-apprentice/jd-rust) 115 | - [liquid](https://github.com/Shopify/liquid) -------------------------------------------------------------------------------- /src/components/card_article.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | components::{icons::StrToIcon, markdown_render::MarkdownRender}, 3 | models::article::Article, 4 | }; 5 | use leptos::{component, view, CollectView, IntoView}; 6 | 7 | #[must_use] 8 | #[component] 9 | pub fn CardArticle(article: Article, is_home: bool) -> impl IntoView { 10 | let article_link = get_link(&article, is_home); 11 | let description = get_description(&article); 12 | 13 | view! { 14 | <> 15 | <li class="group flex flex-col gap-y-1 border border-black p-2 sm:p-6 hover:bg-orange-500 dark:hover:bg-zinc-900/40 bg-orange-100 dark:bg-black/40 drop-shadow-[0_0_0_rgba(0,0,0)] hover:drop-shadow-[-4px_-4px_0_rgba(0,0,0)] transition justify-between"> 16 | <a href=article_link.clone()> 17 | <h3 class="text-xl font-semibold">{article.title}</h3> 18 | </a> 19 | <p>{article.date_string}</p> 20 | <div class="text-sm markdown-container"> 21 | <MarkdownRender content=description /> 22 | </div> 23 | <div> 24 | <span class="pt-4 font-bold">Tags:</span> 25 | <TagsList tags=article.tags /> 26 | </div> 27 | <div class="flex justify-end items-end"> 28 | <a 29 | class="bg-orange-500 group/button hover:bg-orange-600 text-white font-semibold py-2 px-4 rounded flex items-center justify-between gap-2" 30 | href=article_link 31 | > 32 | <span class="group-hover/button:underline">"Leer más"</span> 33 | <StrToIcon 34 | v="next" 35 | class="fill-white group-hover/button:translate-x-1 duration-100" 36 | size=16 37 | /> 38 | </a> 39 | </div> 40 | </li> 41 | </> 42 | } 43 | } 44 | 45 | fn get_link(article: &Article, is_home: bool) -> String { 46 | if is_home { 47 | format!("./articles/{}.html", article.slug) 48 | } else { 49 | format!("./../articles/{}.html", article.slug) 50 | } 51 | } 52 | 53 | fn get_description(article: &Article) -> String { 54 | if article.description.is_empty() { 55 | let binding = article.content.clone(); 56 | let mut content = binding 57 | .split('\n') 58 | .take(3) 59 | .collect::<Vec<&str>>() 60 | .join("\n"); 61 | if content.len() > 190 { 62 | content = content[0..190].to_string(); 63 | content.push_str("..."); 64 | } 65 | content 66 | } else { 67 | article.description.clone() 68 | } 69 | } 70 | 71 | #[must_use] 72 | #[component] 73 | pub fn TagsList(tags: Option<Vec<String>>) -> impl IntoView { 74 | let tags = tags.unwrap_or_default(); 75 | 76 | view! { 77 | <ul class="flex gap-1 py-4"> 78 | {tags.into_iter().map(|tag| TagButton(tag.into())).collect_view()} 79 | </ul> 80 | } 81 | } 82 | 83 | #[component] 84 | #[allow(clippy::needless_pass_by_value)] 85 | #[must_use] 86 | pub fn TagButton(tag: String) -> impl IntoView { 87 | let tag = tag.to_lowercase().replace(' ', "-"); 88 | 89 | view! { 90 | <li class="inline-block text-sm font-bold text-orange-500 hover:text-orange-600"> 91 | <a 92 | class="inline-block bg-white rounded-md p-1 drop-shadow-sm px-2" 93 | href=format!("/tags/{}.html", tag) 94 | > 95 | {tag} 96 | </a> 97 | </li> 98 | } 99 | } 100 | 101 | impl From<String> for TagButtonProps { 102 | fn from(tag: String) -> Self { 103 | TagButtonProps { tag } 104 | } 105 | } 106 | 107 | impl From<(Article, bool)> for CardArticleProps { 108 | fn from((article, is_home): (Article, bool)) -> Self { 109 | CardArticleProps { article, is_home } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.github/workflows/this_week_in_rust.yml: -------------------------------------------------------------------------------- 1 | name: Generate This Week in Rust 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | CUSTOM_DATE: 6 | type: string 7 | description: En caso de que se necesite ejecutar una fecha puntual 8 | schedule: 9 | - cron: "0 0 * * 6" 10 | 11 | jobs: 12 | get: 13 | name: Get Original article 14 | runs-on: ubuntu-22.04 15 | outputs: 16 | content: ${{ steps.output_file.outputs.content }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Define Current Date 20 | run: | 21 | tmp_date=${{ github.event.inputs.CUSTOM_DATE }} 22 | if [ ! -z "$tmp_date" -a "$tmp_date" != " " ]; then 23 | echo "DATE=$tmp_date">>$GITHUB_ENV 24 | exit 0 25 | fi 26 | export CURR_DATE=$(date -d "3 days ago" +%F) 27 | echo "DATE=$CURR_DATE">>$GITHUB_ENV 28 | - name: Download the Article 29 | id: download_article 30 | run: | 31 | wget "https://raw.githubusercontent.com/rust-lang/this-week-in-rust/master/content/$DATE-this-week-in-rust.md" -O "$DATE-tmp.md" 32 | - name: Remove Unnecesary Lines 33 | run: tail -n +6 "$DATE-tmp.md">$DATE.md 34 | # Translate process 35 | - uses: actions/setup-python@v4 36 | with: 37 | python-version: "3.10" 38 | - name: Create Translated File 39 | run: | 40 | week=$(ls esta_semana_en_rust | wc -l); 41 | week=$((week + 1)); 42 | echo "---">"$DATE-this-week-in-rust.md" 43 | echo "title: \"Esta semana en Rust #$week\"">>"$DATE-this-week-in-rust.md" 44 | echo "number_of_week: $week">>"$DATE-this-week-in-rust.md" 45 | echo "description: Esta semana en Rust es un blog semanal sobre el lenguaje de programación Rust, sus comunidades y su ecosistema.">>"$DATE-this-week-in-rust.md" 46 | echo "date: $DATE">>"$DATE-this-week-in-rust.md" 47 | echo "tags:">>"$DATE-this-week-in-rust.md" 48 | echo " - rust">>"$DATE-this-week-in-rust.md" 49 | echo " - comunidad">>"$DATE-this-week-in-rust.md" 50 | echo ' - "esta semana en rust"'>>"$DATE-this-week-in-rust.md" 51 | echo -e "---\n">>"$DATE-this-week-in-rust.md" 52 | - name: Show folder! 53 | run: | 54 | ls -la 55 | echo "----" 56 | echo "Seteo permisos" 57 | chmod 777 "./$DATE-this-week-in-rust.md" 58 | echo "----" 59 | ls -la 60 | - name: Translate File 61 | id: translation_step 62 | env: 63 | API_ENDPOINT: ${{ secrets.MS_TRANSLATE_API_ENDPOINT }} 64 | API_KEY: ${{ secrets.MS_TRANSLATE_API_KEY }} 65 | REGION: ${{ secrets.MS_TRANSLATE_REGION }} 66 | run: | 67 | pip install uuid requests 68 | python3 gen_translated.py 69 | - name: Fixes 70 | run: | 71 | sed -i -e 's/## Actualizaciones de la comunidad de Rust/## Actualizaciones de la comunidad de Rust 🥰/g' "$DATE-this-week-in-rust.md" 72 | sed -i -e 's/] (http/](http/g' "$DATE-this-week-in-rust.md" 73 | sed -i -e 's/Óxido/Rust/g' "$DATE-this-week-in-rust.md" 74 | sed -i -e 's/óxido/Rust/g' "$DATE-this-week-in-rust.md" 75 | sed -i -e 's/\[Equipo de la comunidad de Rust\] \[comunidad\]/[Equipo de la comunidad de Rust][comunidad]/g' "$DATE-this-week-in-rust.md" 76 | sed -i -e 's/envíanos una solicitud de extracción/envíanos un PR/g' "$DATE-this-week-in-rust.md" 77 | sed -i -e 's/## Caja de la semana/## Crate de la semana/g' "$DATE-this-week-in-rust.md" 78 | sed -i -e 's/La caja de esta semana/El crate de esta semana/g' "$DATE-this-week-in-rust.md" 79 | mv "$DATE-this-week-in-rust.md" "./esta_semana_en_rust/$DATE-this-week-in-rust.md" 80 | - name: Commit report 81 | run: | 82 | week=$(ls esta_semana_en_rust | wc -l); 83 | git config --local user.email "action@github.com" 84 | git config --local user.name "GitHub Action" 85 | git add esta_semana_en_rust/*.md 86 | git commit -am "Semana $week publicada" 87 | git push 88 | -------------------------------------------------------------------------------- /src/components/esta_semana_en_rust/layout.rs: -------------------------------------------------------------------------------- 1 | use chrono::Datelike; 2 | use leptos::{component, view, Children, IntoView}; 3 | 4 | use crate::{ 5 | components::esta_semana_en_rust::header::Header, 6 | meta::{Head, Html}, 7 | }; 8 | 9 | fn get_year() -> i32 { 10 | chrono::Utc::now().year() 11 | } 12 | 13 | // This is a common Layout component that will be used by all pages. 14 | #[component] 15 | #[must_use] 16 | pub fn Layout( 17 | #[prop(into, default=format!("Blog de Rust Lang en Español {}", get_year()))] title: String, 18 | #[prop(into, default="Somos una comunidad de Rust hispana, buscamos la promoción del lenguaje de programación Rust.".to_string())] 19 | slug: String, 20 | description: String, 21 | children: Children, 22 | ) -> impl IntoView { 23 | view! { 24 | <Html attrs=vec![("lang", "es")] class="bg-slate-800" /> 25 | <Head> 26 | <meta charset="utf-8" /> 27 | <title>{title.clone()} 28 | 29 | 30 | 31 | 32 | 36 | 37 | 41 | 45 | 46 | 47 | 48 | 49 | {if cfg!(debug_assertions) { 50 | view! { } 51 | } else { 52 | view! { } 53 | }} 54 | 55 | 62 | 73 | 81 | 82 | 83 | // Async is a component from the async_component module. 84 | // It will wrap an async function that returns an IntoView. 85 |
86 |
87 | 88 | // 89 |
{children()}
90 |
91 | } 92 | } 93 | -------------------------------------------------------------------------------- /preview_generator/src/this_week.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use image::{Pixel, Rgba, RgbaImage}; 4 | use rusttype::{Font, Scale}; 5 | 6 | use crate::components::circle_tag; 7 | use crate::models::Article; 8 | use crate::utils::{append_image, chunked_string}; 9 | use crate::{PreviewGenerator, WIDTH}; 10 | 11 | pub struct ThisWeekGenerator { 12 | rustlanges: RgbaImage, 13 | banner: RgbaImage, 14 | } 15 | 16 | impl Default for ThisWeekGenerator { 17 | fn default() -> Self { 18 | let banner = image::open("assets/RustLangES.png").unwrap().to_rgba8(); 19 | 20 | Self { 21 | rustlanges: image::imageops::resize( 22 | &banner, 23 | 85, 24 | 85, 25 | image::imageops::FilterType::Nearest, 26 | ), 27 | banner: image::open("assets/banner.png").unwrap().to_rgba8(), 28 | } 29 | } 30 | } 31 | 32 | impl PreviewGenerator for ThisWeekGenerator { 33 | fn gen( 34 | &self, 35 | img: &mut RgbaImage, 36 | file_name: String, 37 | font: &Font, 38 | bold: &Font, 39 | article: Article, 40 | output: &str, 41 | ) { 42 | let top = 34; 43 | let (padding_x, padding_y) = (30i32, 36i32); 44 | 45 | let title_size = 36.; 46 | let description_size = 60.; 47 | 48 | let _max_title_chars = 30; 49 | let max_word_wrap = 6; 50 | 51 | let dark_color = Rgba::from_slice(&[253, 186, 116, 255]); 52 | let tag_color = Rgba::from_slice(&[0xF5, 0xC9, 0x98, 255]); 53 | let text_color = Rgba::from_slice(&[0x69, 0x37, 0x00, 255]); 54 | 55 | // Paint Background 56 | image::imageops::vertical_gradient(img, dark_color, dark_color); 57 | 58 | // Banner Image 59 | append_image(img, &self.banner, 0, 0, 225); 60 | 61 | // Comunity Image 62 | append_image( 63 | img, 64 | &self.rustlanges, 65 | padding_x as u32, 66 | top as u32 + padding_y as u32, 67 | 225, 68 | ); 69 | 70 | // Title 71 | imageproc::drawing::draw_text_mut( 72 | img, 73 | text_color.clone(), 74 | padding_x * 2 + 80, 75 | top + padding_y + 10, 76 | Scale::uniform(title_size), 77 | bold, 78 | &format!( 79 | "Semana En Rust #{}", 80 | article.title.split_once("#").map(|t| t.1).unwrap() 81 | ), 82 | ); 83 | 84 | // Date 85 | imageproc::drawing::draw_text_mut( 86 | img, 87 | text_color.clone(), 88 | padding_x * 2 + 80, 89 | top + padding_y + title_size as i32 + 10, 90 | Scale::uniform(26.), 91 | font, 92 | &article.date.unwrap_or("".to_string()).replace("-", " / ") 93 | ); 94 | 95 | if let Some(tags) = article.tags.as_ref() { 96 | let mut x = WIDTH as i32 / 2 + 54; 97 | let y = top + padding_y + 22; 98 | 99 | for tag in tags.iter() { 100 | let (w, _) = circle_tag( 101 | img, 102 | bold, 103 | 24., 104 | tag_color.clone(), 105 | text_color.clone(), 106 | (x, y), 107 | (8, 8), 108 | tag.to_string(), 109 | ); 110 | x += 48 + w; 111 | } 112 | } 113 | 114 | // Description 115 | for (i, s) in chunked_string(article.description, max_word_wrap, 5) 116 | .iter() 117 | .enumerate() 118 | { 119 | imageproc::drawing::draw_text_mut( 120 | img, 121 | text_color.clone(), 122 | padding_x, 123 | top + padding_y + 168 + (description_size as i32 * i as i32 + 1), 124 | Scale::uniform(description_size), 125 | bold, 126 | &s, 127 | ); 128 | } 129 | 130 | // Save 131 | let mut output = PathBuf::from(&output); 132 | output.push(format!("{file_name}.png")); 133 | 134 | println!("{output:?}"); 135 | img.save_with_format(output, image::ImageFormat::Png) 136 | .unwrap(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/models/hashnode_article.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub struct HashnodeResponse { 5 | pub data: Option, 6 | pub errors: Option>, 7 | } 8 | 9 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct Data { 12 | pub publication: Publication, 13 | } 14 | 15 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct Publication { 18 | pub posts: Posts, 19 | } 20 | 21 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct Posts { 24 | pub edges: Vec, 25 | } 26 | 27 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 28 | #[serde(rename_all = "camelCase")] 29 | pub struct Edge { 30 | pub node: HashNodeArticle, 31 | } 32 | 33 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct HashNodeArticle { 36 | pub slug: String, 37 | pub title: String, 38 | pub tags: Vec, 39 | pub published_at: String, 40 | pub content: Content, 41 | pub brief: String, 42 | pub publication: Publication2, 43 | } 44 | 45 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct Tag { 48 | pub name: String, 49 | } 50 | 51 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 52 | #[serde(rename_all = "camelCase")] 53 | pub struct Content { 54 | pub markdown: String, 55 | } 56 | 57 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 58 | #[serde(rename_all = "camelCase")] 59 | pub struct Publication2 { 60 | pub links: Links, 61 | pub author: Author, 62 | } 63 | 64 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 65 | #[serde(rename_all = "camelCase")] 66 | pub struct Links { 67 | pub hashnode: String, 68 | pub website: String, 69 | pub github: String, 70 | pub twitter: String, 71 | } 72 | 73 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 74 | #[serde(rename_all = "camelCase")] 75 | pub struct Author { 76 | pub username: String, 77 | } 78 | 79 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 80 | // #[serde(rename_all = "camelCase")] 81 | // pub struct User { 82 | // pub publication: Publication, 83 | // } 84 | 85 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 86 | // #[serde(rename_all = "camelCase")] 87 | // pub struct Publication { 88 | // pub posts: Vec, 89 | // } 90 | 91 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 92 | // #[serde(rename_all = "camelCase")] 93 | // pub struct Post { 94 | // pub slug: String, 95 | // } 96 | 97 | // #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 98 | // #[serde(rename_all = "camelCase")] 99 | // pub struct ArticleFetched { 100 | // pub data: Option, 101 | // pub errors: Option>, 102 | // } 103 | 104 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 105 | // #[serde(rename_all = "camelCase")] 106 | // pub struct ArticleFetchedData { 107 | // pub post: ArticleFetchedPost, 108 | // } 109 | 110 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 111 | // #[serde(rename_all = "camelCase")] 112 | // pub struct ArticleFetchedPost { 113 | // pub slug: String, 114 | // pub title: String, 115 | // pub tags: Vec, 116 | // pub date_added: String, 117 | // pub content_markdown: String, 118 | // pub brief: String, 119 | // pub publication: ArticleFetchedPublication, 120 | // } 121 | 122 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 123 | // #[serde(rename_all = "camelCase")] 124 | // pub struct Tag { 125 | // pub name: String, 126 | // } 127 | 128 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 129 | // #[serde(rename_all = "camelCase")] 130 | // pub struct ArticleFetchedPublication { 131 | // pub links: Links, 132 | // pub username: String, 133 | // } 134 | 135 | // #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 136 | // #[serde(rename_all = "camelCase")] 137 | // pub struct Links { 138 | // pub hashnode: String, 139 | // pub website: String, 140 | // pub github: String, 141 | // pub twitter: String, 142 | // } 143 | -------------------------------------------------------------------------------- /preview_generator/src/blog.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use image::{Pixel, Rgba, RgbaImage}; 4 | use imageproc::rect::Rect; 5 | use rusttype::{Font, Scale}; 6 | 7 | use crate::components::rounded_tag; 8 | use crate::models::Article; 9 | use crate::utils::{append_image, chunked_string}; 10 | use crate::{PreviewGenerator, HEIGHT, WIDTH}; 11 | 12 | pub struct BlogGenerator { 13 | rustlanges: RgbaImage, 14 | user: RgbaImage, 15 | tag: RgbaImage, 16 | } 17 | 18 | impl Default for BlogGenerator { 19 | fn default() -> Self { 20 | Self { 21 | rustlanges: image::open("assets/RustLangES.png").unwrap().to_rgba8(), 22 | user: image::open("assets/user.png").unwrap().to_rgba8(), 23 | tag: image::open("assets/tag.png").unwrap().to_rgba8(), 24 | } 25 | } 26 | } 27 | 28 | impl PreviewGenerator for BlogGenerator { 29 | fn gen( 30 | &self, 31 | img: &mut RgbaImage, 32 | file_name: String, 33 | font: &Font, 34 | bold: &Font, 35 | article: Article, 36 | output: &str, 37 | ) { 38 | let (padding_x, padding_y) = (64, 32); 39 | 40 | let title_size = 64.; 41 | let description_size = 48.; 42 | 43 | let max_title_chars = 30; 44 | let max_word_wrap = 6; 45 | 46 | let bg_color = Rgba::from_slice(&[254, 215, 170, 255]); 47 | let dark_color = Rgba::from_slice(&[253, 186, 116, 255]); 48 | let text_color = Rgba::from_slice(&[0, 0, 0, 255]); 49 | let tag_bg_color = Rgba::from_slice(&[255, 255, 255, 255]); 50 | let tag_text_color = Rgba::from_slice(&[249, 115, 22, 255]); 51 | 52 | // Paint Background 53 | image::imageops::vertical_gradient(img, bg_color, bg_color); 54 | 55 | // Build Top 56 | // Title 57 | imageproc::drawing::draw_text_mut( 58 | img, 59 | text_color.clone(), 60 | padding_x, 61 | padding_y, 62 | Scale::uniform(title_size), 63 | bold, 64 | &article 65 | .title 66 | .get(..max_title_chars) 67 | .or_else(|| Some(&article.title)) 68 | .map(|s| { 69 | if s.len() >= max_title_chars { 70 | format!("{s}...") 71 | } else { 72 | s.to_string() 73 | } 74 | }) 75 | .unwrap(), 76 | ); 77 | // Description 78 | for (i, s) in chunked_string(article.description, max_word_wrap, 3) 79 | .iter() 80 | .enumerate() 81 | { 82 | imageproc::drawing::draw_text_mut( 83 | img, 84 | text_color.clone(), 85 | padding_x, 86 | padding_y + title_size as i32 + 28 + (description_size as i32 * i as i32 + 1), 87 | Scale::uniform(description_size), 88 | font, 89 | &s, 90 | ); 91 | } 92 | 93 | // Build Bottom 94 | imageproc::drawing::draw_filled_rect_mut( 95 | img, 96 | Rect::at(0, HEIGHT as i32 / 2).of_size(WIDTH, HEIGHT / 2), 97 | dark_color.clone(), 98 | ); 99 | // User Section 100 | append_image( 101 | img, 102 | &self.user, 103 | padding_x as u32, 104 | padding_y as u32 + (HEIGHT / 2) + 48, 105 | 255, 106 | ); 107 | imageproc::drawing::draw_text_mut( 108 | img, 109 | text_color.clone(), 110 | padding_x + 65, 111 | padding_y + (HEIGHT as i32 / 2) + 48, 112 | Scale::uniform(description_size), 113 | font, 114 | &article.author.unwrap_or("Desconocido".to_string()), 115 | ); 116 | 117 | // Tags Section 118 | if let Some(tags) = article.tags.as_ref() { 119 | let mut x = padding_x + 65; 120 | let y = padding_y + (HEIGHT as i32 / 2) + 12 + 48 * 2; 121 | 122 | append_image(img, &self.tag, padding_x as u32, y as u32, 255); 123 | 124 | for tag in tags.iter() { 125 | let (w, _) = rounded_tag( 126 | img, 127 | font, 128 | 24., 129 | tag_bg_color.clone(), 130 | tag_text_color.clone(), 131 | 8, 132 | (x, y), 133 | (8, 8), 134 | tag.to_string(), 135 | ); 136 | x += 12 + w; 137 | } 138 | } 139 | 140 | // Comunity Image 141 | let x_min = WIDTH - padding_x as u32 - self.rustlanges.width(); 142 | let y_min = HEIGHT - (HEIGHT / 4) - self.rustlanges.height() / 2; 143 | append_image(img, &self.rustlanges, x_min, y_min, 115); 144 | 145 | // Save 146 | let mut output = PathBuf::from(&output); 147 | output.push(format!("{file_name}.png")); 148 | 149 | println!("{output:?}"); 150 | img.save_with_format(output, image::ImageFormat::Png) 151 | .unwrap(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /.github/workflows/pr-preview.yml: -------------------------------------------------------------------------------- 1 | #################### 🚧 WARNING: READ THIS BEFORE USING THIS FILE 🚧 #################### 2 | # 3 | # 4 | # 5 | # IF YOU DON'T KNOW WHAT YOU'RE DOING, YOU CAN EASILY LEAK SECRETS BY USING A 6 | # `pull_request_target` WORKFLOW INSTEAD OF `pull_request`! SERIOUSLY, DO NOT 7 | # BLINDLY COPY AND PASTE THIS FILE WITHOUT UNDERSTANDING THE FULL IMPLICATIONS 8 | # OF WHAT YOU'RE DOING! WE HAVE TESTED THIS FOR OUR OWN USE CASES, WHICH ARE 9 | # NOT NECESSARILY THE SAME AS YOURS! WHILE WE AREN'T EXPOSING ANY OF OUR SECRETS, 10 | # ONE COULD EASILY DO SO BY MODIFYING OR ADDING A STEP TO THIS WORKFLOW! 11 | # 12 | # 13 | # 14 | #################### 🚧 WARNING: READ THIS BEFORE USING THIS FILE 🚧 #################### 15 | 16 | name: Docs - Preview Deployment 17 | on: 18 | pull_request_target: 19 | types: 20 | - opened 21 | - synchronize 22 | - closed 23 | 24 | # cancel in-progress runs on new commits to same PR (github.event.number) 25 | concurrency: 26 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | deploy-preview: 31 | if: ${{ github.event.action != 'closed' }} 32 | permissions: 33 | contents: read 34 | pull-requests: write 35 | deployments: write 36 | runs-on: ubuntu-latest 37 | name: Deploy Preview to Cloudflare Pages 38 | env: 39 | BRANCH_NAME: preview-${{ github.head_ref }} 40 | ACTION_RUN: ${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}} 41 | steps: 42 | - uses: actions/checkout@v3 43 | with: 44 | submodules: "recursive" 45 | ref: ${{ github.event.pull_request.head.ref }} 46 | repository: ${{ github.event.pull_request.head.repo.full_name }} 47 | - name: Declare some variables 48 | shell: bash 49 | run: | 50 | echo "SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" 51 | 52 | - name: Create comment 53 | id: comment 54 | uses: peter-evans/create-or-update-comment@v4 55 | with: 56 | issue-number: ${{ github.event.pull_request.number }} 57 | comment-author: 'github-actions[bot]' 58 | body: | 59 | ## ⚡ Cloudflare Pages Deployment 60 | | Name | Status | Preview | 61 | | :--- | :----- | :------ | 62 | | ${{env.BRANCH_NAME}} | 🔨 Building ([Logs](${env.ACTION_RUN})) | waiting... | 63 | 64 | # Build Rust Page 65 | - uses: dtolnay/rust-toolchain@stable 66 | - uses: Swatinem/rust-cache@v2 67 | - name: Build 68 | run: npm i && npx tailwindcss -i ./input.css -o ./out/blog/output.css && RUST_BACKTRACE=1 cargo run --release 69 | - name: Generate Previews 70 | run: | 71 | cd preview_generator 72 | cargo run --release -- blog ../articles ../out/blog/articles 73 | cargo run --release -- this_week ../esta_semana_en_rust/ ../out/blog/articles 74 | 75 | - name: Deploy 76 | id: deploy 77 | uses: cloudflare/wrangler-action@v3 78 | with: 79 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 80 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 81 | command: pages deploy ./out/blog --project-name=blog --branch="${{ env.BRANCH_NAME }}" 82 | 83 | - name: Create comment 84 | uses: peter-evans/create-or-update-comment@v4 85 | with: 86 | issue-number: ${{ github.event.pull_request.number }} 87 | comment-id: ${{ steps.comment.outputs.comment-id }} 88 | edit-mode: replace 89 | body: | 90 | ## ⚡ Cloudflare Pages Deployment 91 | | Name | Status | Preview | 92 | | :--- | :----- | :------ | 93 | | ${{env.BRANCH_NAME}} | ✅ Ready ([Logs](${{env.ACTION_RUN}})) | [${{env.SHA_SHORT}}](${{ steps.deploy.outputs.deployment-url }}) | 94 | 95 | # remove-preview: 96 | # if: ${{ github.event.action == "closed" }} 97 | # permissions: 98 | # contents: read 99 | # pull-requests: write 100 | # deployments: write 101 | # runs-on: ubuntu-latest 102 | # name: Remove Preview of Cloudflare Pages 103 | # steps: 104 | # - uses: actions/checkout@v3 105 | # with: 106 | # submodules: "recursive" 107 | # ref: ${{ github.event.pull_request.head.ref }} 108 | # repository: ${{ github.event.pull_request.head.repo.full_name }} 109 | 110 | # - name: Deploy 111 | # id: deploy 112 | # uses: cloudflare/wrangler-action@v3 113 | # with: 114 | # apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 115 | # accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 116 | # command: pages --project-name=homepage --branch="${ env.BRANCH_NAME }" 117 | 118 | # - name: Create comment 119 | # uses: peter-evans/create-or-update-comment@v4 120 | # with: 121 | # issue-number: ${{ github.event.pull_request.number }} 122 | # comment-author: 'github-actions[bot]' 123 | # body: | 124 | # ## ⚡ Removing Cloudflare Pages Preview 125 | # | Name | Status | 126 | # | :--- | :----- | 127 | # | ${{env.BRANCH_NAME}} | ✅ Removed | 128 | -------------------------------------------------------------------------------- /src/components/layout.rs: -------------------------------------------------------------------------------- 1 | use chrono::Datelike; 2 | use leptos::{component, view, Children, IntoView}; 3 | 4 | use crate::{ 5 | components::Header, 6 | meta::{Head, Html}, 7 | }; 8 | 9 | fn get_year() -> i32 { 10 | chrono::Utc::now().year() 11 | } 12 | 13 | // This is a common Layout component that will be used by all pages. 14 | #[component] 15 | #[must_use] 16 | pub fn Layout( 17 | #[prop(into, default=format!("Blog de Rust Lang en Español {}", get_year()))] title: String, 18 | #[prop(into, default="rustlanges_preview.webp".to_string())] slug: String, 19 | #[prop(into, default = false)] is_home: bool, 20 | #[prop(into, default="Somos una comunidad de Rust hispana, buscamos la promoción del lenguaje de programación Rust.".to_string())] 21 | description: String, 22 | children: Children, 23 | ) -> impl IntoView { 24 | view! { 25 | 29 | 30 | 31 | {title.clone()} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | {if is_home { 45 | view! { 46 | <> 47 | 48 | 52 | 56 | 57 | } 58 | } else { 59 | view! { 60 | <> 61 | 62 | 66 | 70 | 71 | } 72 | }} 73 | 74 | 75 | 76 | 77 | {if cfg!(debug_assertions) { 78 | view! { } 79 | } else { 80 | view! { } 81 | }} 82 | 83 | 91 | 102 | 110 | 111 | 112 | // Async is a component from the async_component module. 113 | // It will wrap an async function that returns an IntoView. 114 |
115 |
116 | 117 | // 118 |
{children()}
119 |
120 | } 121 | } 122 | -------------------------------------------------------------------------------- /articles/variables-y-declaraciones.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Variables y declaraciones en Rust 3 | description: La declaración de variables es algo fundamental en el aprendizaje de Rust, en este articulo abordaremos una guía rápida. 4 | author: Fernando Pastorelli 5 | github_user: Phosphorus-M 6 | date: 2024-02-10 7 | tags: 8 | - rust 9 | - data-type 10 | - roadmap 11 | - principiante 12 | social: 13 | github: https://github.com/Phosphorus-M 14 | twitter: https://twitter.com/Phosphorus_M 15 | website: https://phosphorus-moscu.gitlab.io 16 | --- 17 | 18 | En Rust a la hora de programar usaremos infinidad de veces algo llamado variables. 19 | 20 | Una variable es un contenedor que almacena un valor o información en un programa de computadora. Estos valores pueden ser números, texto, lógicos como verdadero o falso, u otros tipos de información. 21 | 22 | Veamos el siguiente ejemplo. 23 | ```rust 24 | let mi_numero = 2; 25 | ``` 26 | 27 | En Rust para crear nuevas variables usamos la palabra clave `let`, con esa palabra crearemos a todas las variables existentes. 28 | Como se observa seguido de la palabra clave `let`se le dara un nombre a la variable, un identificador. 29 | Ese identificador usaremos en el resto del código para referirnos a esa variable. 30 | Seguido usaremos el `=`para asignar un valor inicial. 31 | En este caso 2. 32 | 33 | Cuando usemos la variable `mi_numero` obtendremos el valor 2. 34 | 35 | Otra cosa que nos podría interesar es cambiar el valor de la variable a medida que lo usemos. 36 | Eso debido a que quizás queremos re utilizar la misma variable. 37 | Por ejemplo quiero usar la variable para guardar mi numero preferido. 38 | Inicialmente era 2, pero ahora cambie de opinion. 39 | Eso se reflejaría en el código de la siguiente manera 40 | 41 | ```rust 42 | let mut mi_numero = 2; 43 | // ... más código 44 | mi_numero = 7; 45 | ``` 46 | 47 | Ahora se cambio, `mi_numero`pasa a ser 7. 48 | Eso se logro porque nosotros usamos previamente, en la declaración de la variable como mut. 49 | Es decir como mutable. 50 | En la programación un problema común es que los desarrolladores crean variables y suele re utilizarlas bastante sin saber su uso en otro momento. 51 | Por lo que inicialmente se espera que tenga un valor pero en entre un punto y otro alguien modifica ese valor, eso crea un error, se esperaba que ese valor permanezca con el estado inicial a lo largo de su ejecución. 52 | En Rust, para solucionar ese problema se tomo la medida de que toda variable sea inmutable por defecto. 53 | Es decir, a menos que no sea declarada para que sea modificada por futuros programadores esa variable no puede cambiar de valor. 54 | 55 | Además de esto Rust infiere el tipo de dato. 56 | En muchos lenguajes a la hora de crear una variable se debe definir el tipo de dato, es decir que tipo de valor va a contener la variable, numérico, de texto o booleano. 57 | ¿Por que? Porque perfectamente se podría tener algo como: 58 | ```rust 59 | let mi_numero = "2"; 60 | ``` 61 | Eso causaría confusión posiblemente en algunas personas, ¿Debo entender que `mi_numero ` es realmente un numero o es un texto que tiene el numero? 62 | Peor aún si esto lo cambiamos por 1 (representación numérica de verdadero), por lo que podría causar confusión. 63 | Estos pueden ser casos muy pequeños pero entre más se complejiza el código y hay nuevos tipos de datos, se requiere más y más definir como debemos interpretar una variable. 64 | 65 | Rust para resolver esto tiene la inferencia. 66 | La inferencia significa que todos los valores en Rust tienen uno o muchos tipos por defecto. 67 | Eso significa que para casos muy simples como: 68 | ```rust 69 | let mi_numero = 2; 70 | ``` 71 | No habría necesidad de tipar. 72 | Tipar es opcional. 73 | Pero en Rust hay varios tipos de datos numéricos, por defecto los enteros son de tipo `i32`. 74 | Quiere decir Integer (Entero en español) de 32 bits, esto cobrara sentido más adelante.. 75 | Para definir un tipo usamos los `:` seguido del tipo de dato que seria. 76 | ```rust 77 | let mi_numero:i32 = 2; 78 | ``` 79 | No es necesario hacer esto para este caso el `i32` no tendría mucho sentido tiparlo pero podríamos desear que sea de tipo `u8` en lugar de `i32`. 80 | ¿Por qué? Eso lo veremos cuando veamos esos tipos de datos. 81 | 82 | Lo que podemos decir es que de momento para hacerlo de tipo `u8` podríamos usar: 83 | ```rust 84 | let mi_numero:u8 = 2; 85 | ``` 86 | De esta forma si bien el valor seria el mismo el tipo de dato de `mi_numero` habría cambiado. 87 | Otra cosa que podría resultar interesante es el uso de : 88 | 89 | ### const 90 | 91 | En Rust la palabra clave `const` se utiliza para declarar constantes. 92 | Al igual que las variables son inmutables pero a diferencia de ellas no se pueden cambiar a mutables. 93 | Y a diferencia de las variables cuando se usa una constante siempre debemos definir el tipo de dato que tendrá. 94 | Veamos el siguiente ejemplo 95 | 96 | ```rust 97 | const MAYORIA_DE_EDAD: u8 = 18; 98 | 99 | fn main() { 100 | println!("En este país la mayoría de edad es {}", MAYORIA_DE_EDAD); 101 | } 102 | 103 | ``` 104 | Analicemos lo que esta sucediendo. 105 | Estamos declarando una constante fuera del main. 106 | Eso es algo que no hemos hablado. 107 | Las variables suelen ser declaradas en el ámbito de una función. 108 | 109 | No pueden haber variables sueltas. 110 | 111 | En el caso de las constantes se puede declarar fuera de una función. 112 | 113 | Otra característica que quizás notaremos es que las constantes se escriben en mayúsculas. 114 | 115 | Y nos puede llamar la atención que la constante puede ser usada en cualquier lado del código. Sin hacer nada raro, tiene lo que se considera un scope (ámbito) global en nuestro código. 116 | 117 | Quizás el uso más avanzado del que podremos llegar a ver sera el uso de `const` para hacer evaluaciones en tiempo de compilación y quizás sea rutinario ver `const`para valores que nunca, jamás cambiaran. Ni si quiera con un concepto más avanzado como la mutabilidad interna. 118 | 119 | ### static 120 | 121 | Por ultimo quizás debamos de ver el uso de la palabra `static`. 122 | El uso de `static` es justamente para crear valores que van a perdurar toda la vida de la ejecución. 123 | Esto quizás se vea más en profundidad cuando veamos **Lifetimes** de momento retengan en mente el siguiente ejemplo. 124 | 125 | ```rust 126 | // Declara una variable estática global 127 | static GLOBAL_VARIABLE: u8 = 42; 128 | 129 | fn main() { 130 | // Accede a la variable global 131 | println!("El valor de la variable global es: {}", GLOBAL_VARIABLE); 132 | } 133 | ``` 134 | 135 | Si bien podríamos creer que es el mismo caso que una constante, en Rust el uso de `static` habilita la creación de variables globales, al igual que const deberemos de tipar la variable. 136 | Acabo de decir variables globales por que a diferencia de las `const` estas pueden ser modificadas, inicialmente tendrán un valor inmutable, pero nosotros podríamos usar `mut`para volverlas mutable. 137 | Sin embargo esto es desaconsejado. 138 | A lo largo de los años, la experiencia de muchos desarrolladores ha demostrado que esto es una mala practica. 139 | 140 | Rust sin embargo permitirá la modificación de la variable global pero bajo uso de `unsafe`. 141 | ¿Qué es `unsafe`? Lo veremos más adelante. 142 | Hay otra manera que Rust permite la modificación de variables globales. 143 | Esta vez si es un uso recomendado por desarrolladores expertos que es usando Mutabilidad Interna. 144 | 145 | Otro concepto que veremos a lo largo del Roadmap, en el blog y demás. 146 | 147 | Desde ya muchas gracias por leer. -------------------------------------------------------------------------------- /src/models/article.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use chrono::{DateTime, Datelike, FixedOffset, Locale, NaiveDate, NaiveDateTime}; 4 | use rss::{Category, Enclosure, Guid, Item, Source}; 5 | use serde::{Deserialize, Deserializer, Serialize}; 6 | 7 | use super::{devto_article::DevToArticle, hashnode_article::HashNodeArticle}; 8 | 9 | #[derive(Serialize, Deserialize, Clone, Default)] 10 | pub struct Article { 11 | pub title: String, 12 | pub description: String, 13 | #[serde(default)] 14 | pub author: Option, 15 | #[serde(default)] 16 | pub authors: Option>, 17 | pub github_user: Option, 18 | #[serde(default)] 19 | pub slug: String, 20 | #[serde(default)] 21 | pub content: String, 22 | #[serde(default)] 23 | pub tags: Option>, 24 | #[serde(default)] 25 | pub number_of_week: Option, 26 | #[serde( 27 | rename(deserialize = "date"), 28 | deserialize_with = "string_to_naive_date" 29 | )] 30 | pub date: NaiveDate, 31 | #[serde(default)] 32 | pub date_string: Option, 33 | pub social: Option>, 34 | #[serde(default)] 35 | pub devto: bool, 36 | } 37 | 38 | fn string_to_naive_date<'de, D>(de: D) -> Result 39 | where 40 | D: Deserializer<'de>, 41 | D::Error: serde::de::Error, 42 | { 43 | let date_str: String = Deserialize::deserialize(de)?; 44 | let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").unwrap_or_default(); 45 | 46 | Ok(date) 47 | } 48 | 49 | impl From for Article { 50 | fn from(devto_article: DevToArticle) -> Self { 51 | let date_time = 52 | NaiveDate::parse_from_str(&devto_article.published_at, "%Y-%m-%dT%H:%M:%SZ").unwrap(); 53 | 54 | Self { 55 | title: devto_article.title, 56 | description: devto_article.description, 57 | author: Some(devto_article.user.name), 58 | github_user: Some(devto_article.user.github_username.clone()), 59 | date: date_time, 60 | social: Some(HashMap::from([ 61 | ( 62 | "twitter".to_string(), 63 | format!( 64 | "https://twitter.com/{}", 65 | devto_article.user.twitter_username 66 | ), 67 | ), 68 | ( 69 | "github".to_string(), 70 | format!("https://github.com/{}", devto_article.user.github_username), 71 | ), 72 | ])), 73 | slug: devto_article.slug, 74 | date_string: Some( 75 | date_time 76 | .format_localized("%e de %B del %Y", Locale::es_ES) 77 | .to_string(), 78 | ), 79 | content: devto_article.content_html.unwrap_or_default(), 80 | 81 | devto: true, 82 | tags: Some(devto_article.tag_list), 83 | ..Default::default() 84 | } 85 | } 86 | } 87 | 88 | impl From for Article { 89 | fn from(hashnode_article: HashNodeArticle) -> Self { 90 | let date_time = 91 | NaiveDate::parse_from_str(&hashnode_article.published_at, "%Y-%m-%dT%H:%M:%S%.fZ") 92 | .unwrap(); 93 | 94 | Self { 95 | title: hashnode_article.title, 96 | description: hashnode_article.brief, 97 | author: Some(hashnode_article.publication.author.username), 98 | github_user: hashnode_article 99 | .publication 100 | .links 101 | .github 102 | .split('/') 103 | .next_back() 104 | .map(std::string::ToString::to_string), 105 | date: date_time, 106 | social: Some(HashMap::from([ 107 | ( 108 | "twitter".to_string(), 109 | hashnode_article.publication.links.twitter, 110 | ), 111 | ( 112 | "github".to_string(), 113 | hashnode_article.publication.links.github, 114 | ), 115 | ])), 116 | slug: hashnode_article.slug, 117 | content: hashnode_article.content.markdown, 118 | date_string: Some( 119 | date_time 120 | .format_localized("%e de %B del %Y", Locale::es_ES) 121 | .to_string(), 122 | ), 123 | devto: false, 124 | tags: Some( 125 | hashnode_article 126 | .tags 127 | .iter() 128 | .map(|tag| tag.name.clone().to_lowercase().replace(' ', "-")) 129 | .collect(), 130 | ), 131 | ..Default::default() 132 | } 133 | } 134 | } 135 | 136 | impl From<&Article> for Item { 137 | fn from(value: &Article) -> Self { 138 | let date: NaiveDate = 139 | NaiveDate::from_ymd_opt(value.date.year(), value.date.month(), value.date.day()) 140 | .unwrap(); 141 | let datetime: NaiveDateTime = date.and_hms_opt(0, 0, 0).unwrap(); 142 | let offset = FixedOffset::west_opt(5 * 3600).unwrap(); 143 | let datetime_with_timezone = 144 | DateTime::::from_naive_utc_and_offset(datetime, offset); 145 | let formated_datetime_with_timezone = datetime_with_timezone 146 | .format("%a, %d %b %Y %T %z") 147 | .to_string(); 148 | let link = format!( 149 | "https://blog.rustlang-es.org/articles/{}.html", 150 | value.slug.clone() 151 | ); 152 | Item { 153 | title: Some(value.title.clone()), 154 | link: Some(link), 155 | description: Some(value.description.clone()), 156 | author: value.author.clone(), 157 | categories: value 158 | .tags 159 | .clone() 160 | .map(|c| { 161 | c.iter() 162 | .map(|c| Category { 163 | name: c.to_string(), 164 | domain: None, 165 | }) 166 | .collect::>() 167 | }) 168 | .unwrap_or_default(), 169 | guid: Some(Guid { 170 | value: value.slug.clone(), 171 | permalink: false, 172 | }), 173 | pub_date: Some(formated_datetime_with_timezone), 174 | source: Some(Source { 175 | url: "https://github.com/RustLangES/blog".to_string(), 176 | title: Some("Repositorio del Blog".to_string()), 177 | }), 178 | content: None, 179 | enclosure: Some(Enclosure { 180 | url: format!( 181 | "https://blog.rustlang-es.org/articles/{}.png", 182 | value.slug.clone() 183 | ), 184 | length: "626471".to_string(), 185 | mime_type: "image/png".to_string(), 186 | }), 187 | ..Default::default() 188 | } 189 | } 190 | } 191 | 192 | impl Article { 193 | #[must_use] 194 | pub fn has_author(&self) -> bool { 195 | if let Some(author) = &self.author { 196 | !author.is_empty() 197 | } else { 198 | false 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/components/feature_articles.rs: -------------------------------------------------------------------------------- 1 | use leptos::{component, view, IntoView}; 2 | use leptos_mdx::mdx::{Components, MdxComponentProps}; 3 | 4 | use crate::{ 5 | components::{ 6 | icons::StrToIcon, 7 | mdx::{ 8 | center::{Center, CenterProps}, 9 | youtube::{Youtube, YoutubeProps}, 10 | }, 11 | }, 12 | models::article::Article, 13 | ARTICLES, 14 | }; 15 | use leptos_mdx::mdx::Mdx; 16 | 17 | /// # Panics 18 | /// If no article is found with the tag "esta semana en rust", the call to `unwrap()` will panic. 19 | /// If no article is found with the tag "anuncio de la comunidad", the second `unwrap()` will panic. 20 | /// If the "video" attribute is missing in any `YouTube` component, the `unwrap()` inside the closure will panic. 21 | /// If the value of the "video" attribute is None, the second `unwrap()` will also panic. 22 | pub async fn featured_articles() -> impl IntoView { 23 | let articles = ARTICLES.read().await.clone(); 24 | let _invalid_tags = [ 25 | "esta semana en rust".to_string(), 26 | "anuncio de la comunidad".to_string(), 27 | ]; 28 | // Take the first article with the tag "esta semana en rust" 29 | let esta_semana_en_rust = articles 30 | .clone() 31 | .into_iter() 32 | .filter(|article| filter_article_by_tag(article, "esta semana en rust")) 33 | .take(1) 34 | .collect::>(); 35 | let esta_semana_en_rust = esta_semana_en_rust.first().unwrap().to_owned(); 36 | let anuncio_de_la_comunidad = articles 37 | .into_iter() 38 | .filter(|article| filter_article_by_tag(article, "anuncio de la comunidad")) 39 | .take(1) 40 | .collect::>(); 41 | 42 | let anuncio_de_la_comunidad = anuncio_de_la_comunidad.first().unwrap().to_owned(); 43 | 44 | let mut components2 = Components::new(); 45 | components2.add_props( 46 | "youtube".to_string(), 47 | Youtube, 48 | |props: MdxComponentProps| { 49 | let video_id = props.attributes.get("video").unwrap().clone(); 50 | YoutubeProps { 51 | video: video_id.unwrap(), 52 | } 53 | }, 54 | ); 55 | components2.add_props("center".to_string(), Center, |props: MdxComponentProps| { 56 | CenterProps { 57 | children: props.children, 58 | } 59 | }); 60 | view! { 61 |
62 | 63 | 64 | 65 |
66 | } 67 | } 68 | 69 | #[must_use] 70 | pub fn filter_article_by_tag(article: &Article, tag: &str) -> bool { 71 | if let Some(tags) = &article.tags { 72 | tags.contains(&tag.to_string()) 73 | } else { 74 | false 75 | } 76 | } 77 | 78 | #[component] 79 | pub fn EstaSemanaEnRustCard(article: Article) -> impl IntoView { 80 | let mut components = Components::new(); 81 | components.add_props( 82 | "youtube".to_string(), 83 | Youtube, 84 | |props: MdxComponentProps| { 85 | let video_id = props.attributes.get("video").unwrap().clone(); 86 | YoutubeProps { 87 | video: video_id.unwrap(), 88 | } 89 | }, 90 | ); 91 | components.add_props("center".to_string(), Center, |props: MdxComponentProps| { 92 | CenterProps { 93 | children: props.children, 94 | } 95 | }); 96 | 97 | let description = article.description.clone(); 98 | 99 | view! { 100 |
101 |
102 | // Big Header of the card with capital letter and the number of the week of the article 103 |
104 |
105 |
106 |

107 | "Esta semana en Rust" 108 | 109 | # {article.number_of_week.unwrap()} 110 | 111 |

112 |
113 |
114 | 115 |
116 | 129 |
130 |

{article.date_string}

131 |
132 |
133 | 138 |
139 | 152 |
153 | } 154 | } 155 | 156 | #[component] 157 | pub fn AnuncioDeLaComunidadCard(article: Article) -> impl IntoView { 158 | let mut components = Components::new(); 159 | components.add_props( 160 | "youtube".to_string(), 161 | Youtube, 162 | |props: MdxComponentProps| { 163 | let video_id = props.attributes.get("video").unwrap().clone(); 164 | YoutubeProps { 165 | video: video_id.unwrap(), 166 | } 167 | }, 168 | ); 169 | components.add_props("center".to_string(), Center, |props: MdxComponentProps| { 170 | CenterProps { 171 | children: props.children, 172 | } 173 | }); 174 | 175 | let description = article.description.clone(); 176 | 177 | view! { 178 | <> 179 |
180 |

Anuncio:

181 |
182 |

{article.title.clone()}

183 |

{article.date_string}

184 |
185 | 186 |
187 | 200 |
201 |
202 | 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /articles/hicimos-el-sitio-web-de-nuestra-boda-en-angular-y-rust-pk8.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hicimos el sitio web de nuestra boda en Angular y Rust 3 | description: La historia comienza en agosto del año pasado nos comprometimos durante nuestras vacaciones en... 4 | author: Julieta Campos Guzmán 5 | github_user: juliescript 6 | date: 2020-04-24 7 | tags: 8 | - rust 9 | - angular 10 | - devto 11 | - devjournal 12 | social: 13 | github: https://github.com/juliescript 14 | twitter: https://twitter.com/juliescriptdev 15 | website: https://phosphorus-moscu.gitlab.io 16 | --- 17 | 18 | ## La Historia 19 | 20 | En agosto del año pasado nos comprometimos durante nuestras vacaciones en Japón. 21 | 22 | Decidimos planear nuestra boda en México porque aunque vivimos en Alemania, nuestras familias están en México y es donde queremos celebrar con todos nuestros seres queridos. 23 | 24 | Una de las partes más importantes de planear una boda son las invitaciones. Usualmente se hacen de forma física, son cosas muy hermosas y elaboradas que se envían a los invitados. En ellas se encuentran todos los datos sobre la boda como: 25 | 26 | - Fecha 27 | - Lugar 28 | - Hora 29 | - Programa 30 | - Etiqueta 31 | - Mesa de regalos 32 | - Boletos para la recepción 33 | 34 | Hacer invitaciones no es una opción para nosotros. Tenemos que coordinar invitados que vienen de distintas ciudades y de distintos países. Además de que mandarlas a hacer puede ser muy caro y enviarlas es mucho trabajo. 35 | 36 | Por eso fue que decidimos usar nuestras habilidades como desarrolladores y unir fuerzas para crear un sitio web para nuestra boda. 37 | 38 | ## El sitio 39 | 40 | Nuestro sitio va a tener dos funciones principales: 41 | 42 | - Dar la información sobre la boda 43 | - Administrar la asistencia de los invitados 44 | 45 | Así que pensamos en crear un sistema que sirviera para que los invitados confirmen su asistencia y que después podamos enviar la invitación más formal en PDF antes del mero día de la boda. 46 | 47 | Para lograr esto nos dividimos el trabajo. Mi prometido se encargó de hacer todo el backend y yo me encargué de hacer el frontend. Entre los dos decidimos en un diseño y agregamos el contenido a la página. Mi prometido se encargó de traducir los textos porque necesitamos tener el sitio en español e inglés. 48 | 49 | Va sin recalcar que tenemos excepciones para invitados que no saben o que no tienen acceso a la web. 50 | 51 | ### El tech stack 52 | 53 | Para el backend, todo fue manejaod por mi prometido así que no entraré en muchos detalles. 54 | 55 | El lenguaje de programación fue Rust porque es el lenguaje que está usando ahora. 56 | 57 | El stack del backend terminó así: 58 | 59 | - [Rust](https://github.com/ngx-translate/core) 60 | - [Gotham](https://gotham.rs/) - para manejar el API 61 | - [Diesel](http://diesel.rs/) - para conectar y administrar la base de datos 62 | - [PostgreSQL](https://www.postgresql.org/) 63 | - [GitHub Actions](https://github.com/features/actions) 64 | - Hosting en [Digital Ocean](https://www.digitalocean.com/) 65 | 66 | El stack del frontend fue el siguiente: 67 | 68 | - [Angular 9](https://angular.io/) 69 | - [SASS](https://sass-lang.com/) 70 | - Deploy en [Netlify](https://www.netlify.com/) 71 | 72 | Para el manejo de usuarios decidimos usar Facebook y Google login. La verdad no queríamos quedarnos con información personal del usuario y no quisimos lidiar con GDPR. 73 | 74 | ### El proceso 75 | 76 | En el momento en que empezamos a planear el sitio, mi prometido estaba tomando una clase de administración de proyectos web para su maestría. Por mi lado, he tomado varios talleres de generación de ideas y de crear proyectos de forma ágil. 77 | 78 | De nuevo juntamos recursos e hicimos una sesión para definir que es lo que necesitaba la página y que es lo que queríamos lograr. Al final terminamos poniendo todas las tareas en un tablero tipo Kanban en JIRA. Esto nos ayudó mucho a mantener nuestro objetivo en la mira. 79 | 80 | ### El diseño 81 | 82 | El diseño fue decisión principalmente mía. La verdad soy pésima diseñando así que me puse a buscar inspiración en Pinterest y otros sitios como Wix y Squarespace. 83 | 84 | Al final decidí reproducir una plantilla de sitio para boda de Squarespace. El diseño nos gustó mucho porque era sencillo y elegante. El esquema de colores es neutral y no se ve super feminino o masculino. 85 | 86 | Es un diseño bastante sobrio y la tipografía me encantó. 87 | 88 | Además de que ya viene con diseño móvil que siempre es un viacrucis incluir. 89 | 90 | [![Plantilla Squarespace home](/assets/images/julie-y-vic-home.png "Plantilla Squarespace home")](/assets/images/julie-y-vic-home.png) 91 | 92 | [![Plantilla Squarespace about](/assets/images/julie-y-vic-about.jpeg "Plantilla Squarespace about")](/assets/images/julie-y-vic-about.jpeg) 93 | 94 | _Plantilla [Morena](https://morena-demo.squarespace.com/) de Squarespace_ 95 | 96 | A partir del diseño creamos las demás páginas que no estaban definidas. 97 | 98 | No tiene nada de malo reproducir un diseño ya creado si no eres bueno en diseño o si no puedes pagarle a un diseñador. 99 | 100 | ### El frontend 101 | 102 | Jugué con la idea de hacer el frontend con React y Gatsby pero la verdad es que me siento mucho más cómoda con Angular. Puedo resolver problemas mejor y no tengo que sufrir tanto conectándome al backend. 103 | 104 | Además de que estilizar Angular es algo que es un sueño cuando lo haces con SASS. Es mi tech stack favorito y me ha servido bien varios años ya. 105 | 106 | El mapa del sitio quedó de la siguiente manera: 107 | 108 | - Página principal 109 | - Información de la boda 110 | - Información de viaje 111 | - 112 | 113 | RSVP 114 | 115 | - Login 116 | - Redirección de login de facebook 117 | - Página de perfil 118 | - 404 119 | 120 | #### Diseño responsivo 121 | 122 | Hacer sitios responsivos creo que es algo que nos llega a dar mucha flojera a varios programadores. Hay muchas variables y hay que escribir mucho código. Afortunadamente pude usar casi puro CSS para manejar el diseño responsivo. 123 | 124 | La única ocasión donde tuve que incorporar Javascript fue con el menú para dispositivos móviles. Necesitaba manejar cuando activo y desactivo el menú y no me quise complicar la vida. Así que fue con Javascript. 125 | 126 | #### Facebook y Google Login 127 | 128 | Para el manejo de usuario usamos Google y Facebook login. Toda la implementación la hizo mi prometido en Rust, así que del lado del frontend me tocó manejar las redirecciones. 129 | 130 | El flujo que tenemos es el siguiente: 131 | 132 | 1. Usuario recibe un link de invitación con un código único 133 | 2. En la página, el usuario puede elegir entre iniciar sesión con Facebook o con Google 134 | 3. Ya que se inicia la sesión, se redirecciona al usuario de regreso al sitio 135 | 4. El usuario puede elegir si asistirá o no a la boda y si necesita llevar pareja 136 | 137 | ### Traducciones 138 | 139 | Como lo mencioné al principio, necesitamos traducciones para el sitio. Tenía muchas ganas de usar las traducciones nativas de Angular pero me hubiera tomado mucho tiempo configurarlas. 140 | 141 | Decidí ir por un paquete que usé mucho tiempo en mi trabajo anterior llamado [@ngx-translate/core](https://github.com/ngx-translate/core). Este paquete me permite generar variables y mantener los idiomas con base en archivos json. La configuración es muy corta y maneja el cambio de idioma de inmediato y a nivel de aplicación. 142 | 143 | ### El producto terminado 144 | 145 | Al final el sitio terminó así: 146 | 147 | [![Julie y Vic home](/assets/images/julie-y-vic-home.png "Julie y Vic home")](/assets/images/julie-y-vic-home.png) 148 | 149 | [![Julie y Vic about](/assets/images/julie-y-vic-about.jpeg "Julie y Vic about")](![/assets/images/julie-y-vic-about.jpeg)) 150 | 151 | ## Conclusiones 152 | 153 | **¿Lo volvería a hacer?** 154 | 155 | La verdad es que si no fuera por la funcionalidad especial que queríamos para administrar a los usuarios, hubiera utilizado alguna herramienta ya existente para hacerlo. Incluso contratar Squarespace para usar la plantilla que reproduje. 156 | 157 | No queríamos lidiar con procesar formularios a mano o conservar datos de invitados, por eso el login con Facebook o Google fue muy importante y eso es algo que no vimos en otras plataformas para hacer sitios de boda. 158 | 159 | **Altervativas para tu propio sitio de boda** 160 | 161 | Puedes usar una herramienta como [Squarespace](https://www.squarespace.com/) o [Wix](https://www.wix.com/) para hacer un sitio. Incluso hay portales exclusivos de bodas como [The Knot](https://www.theknot.com/) que te permiten crear un pequeño sitio con links a todo lo que necesitas. 162 | 163 | **¿Es necesario?** 164 | 165 | Probablemente no. Si tu boda puede llevarse con invitaciones normales y es la ruta que quieres tomar, es lo único que se necesita. Si quieres manejar todo con un evento de Facebook se puede. Todo depende de lo que quieras para mantener a tus invitados enterados de todos los detalles de tu evento. 166 | 167 | Me gustó mucho la experiencia de hacer el sitio de la boda. Mi prometido y yo nunca habíamos trabajado en un proyecto juntos, así que fue una bonita experiencia. Además esto nos ayudó a pensar en más detalles del evento y organizar a nuestros invitados de mejor manera. 168 | 169 | Lamentablemente tuvimos que posponer la boda debido al COVID-19 pero cuando tengamos una nueva fecha pondremos el sitio en línea. -------------------------------------------------------------------------------- /src/components/blog_content.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | components::{ 5 | icons::StrToIcon, 6 | mdx::{ 7 | center::{Center, CenterProps}, 8 | youtube::{Youtube, YoutubeProps}, 9 | }, 10 | }, 11 | models::article::Article, 12 | }; 13 | use leptos::{component, view, IntoView}; 14 | use leptos_mdx::mdx::{Components, Mdx, MdxComponentProps}; 15 | 16 | #[component] 17 | pub fn BlogContent( 18 | #[prop()] article: Article, 19 | #[prop(default = false)] is_html: bool, 20 | ) -> impl IntoView { 21 | let mut components = Components::new(); 22 | let social = article.social.clone().unwrap_or_default(); 23 | 24 | components.add_props( 25 | "youtube".to_string(), 26 | Youtube, 27 | |props: MdxComponentProps| { 28 | let video_id = props.attributes.get("video").unwrap().clone(); 29 | 30 | YoutubeProps { 31 | video: video_id.unwrap(), 32 | } 33 | }, 34 | ); 35 | 36 | components.add_props("center".to_string(), Center, |props: MdxComponentProps| { 37 | CenterProps { 38 | children: props.children, 39 | } 40 | }); 41 | 42 | view! { 43 |
44 |
45 | 84 |
85 | {ArticleHeader(ArticleHeaderProps { 86 | has_author: article.has_author(), 87 | title: article.title, 88 | github_user: article.github_user, 89 | author: article.author, 90 | social, 91 | tags: article.tags.unwrap_or_default(), 92 | date_string: article.date_string, 93 | })} 94 |
95 | {if is_html { 96 | view! { 97 | <> 98 |
102 | 103 | } 104 | } else { 105 | view! { 106 | <> 107 | 108 | 109 | } 110 | }} 111 | 112 |
113 |
114 |
115 |
116 | } 117 | } 118 | 119 | #[component] 120 | #[must_use] 121 | #[allow(clippy::needless_pass_by_value, clippy::implicit_hasher)] 122 | pub fn ArticleHeader( 123 | #[prop()] title: String, 124 | #[prop()] github_user: Option, 125 | #[prop()] author: Option, 126 | #[prop()] has_author: bool, 127 | #[prop()] social: HashMap, 128 | #[prop()] tags: Vec, 129 | #[prop()] date_string: Option, 130 | ) -> impl IntoView { 131 | view! { 132 | <> 133 |
134 |

135 | {title.clone()} 136 |

137 |
138 | {if github_user.is_some() { 139 | view! { 140 | <> 141 | 148 | 149 | } 150 | } else { 151 | view! { <> } 152 | }} 153 | {if has_author { 154 | view! { 155 | <> 156 | {author} 157 | 158 | } 159 | } else { 160 | view! { <> } 161 | }} 162 | {if tags.is_empty() { 163 | view! { 164 | <> 165 | "habla sobre:" 166 | 167 | } 168 | } else { 169 | view! { <> } 170 | }} 171 | {tags 172 | .iter() 173 | .map(|tag| { 174 | let tag = tag.to_lowercase().replace(' ', "-"); 175 | view! { 176 | 182 | {tag} 183 | 184 | } 185 | }) 186 | .collect::>()} 187 |
188 | {if social.is_empty() { 189 | view! { <> } 190 | } else { 191 | view! { 192 | <> 193 |
194 | 195 | } 196 | }} 197 | {social 198 | .iter() 199 | .map(|(net, url)| { 200 | view! { 201 | 202 | 203 | 204 | } 205 | }) 206 | .collect::>()} 207 |
208 |
209 |
210 | {date_string} 211 | 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod async_component; 2 | pub mod components; 3 | pub mod meta; 4 | pub mod models; 5 | pub mod pages; 6 | pub mod render; 7 | pub mod ssg; 8 | pub mod utils; 9 | 10 | use std::{ 11 | fs::{self, ReadDir}, 12 | path::Path, 13 | sync::LazyLock, 14 | }; 15 | 16 | use futures::future::join_all; 17 | use futures_concurrency::prelude::*; 18 | use gray_matter::{engine::YAML, Matter}; 19 | use models::article::Article; 20 | 21 | use pages::{ 22 | article_page::ArticlePageProps, 23 | esta_semana_en_rust::{EstaSemanaEnRust, EstaSemanaEnRustProps}, 24 | home::HomepageProps, 25 | }; 26 | use ssg::Ssg; 27 | use tokio::sync::RwLock; 28 | use utils::generate_feed_rss; 29 | 30 | use crate::pages::{article_page::ArticlePage, home::Homepage}; 31 | 32 | pub static ARTICLES: LazyLock>> = 33 | LazyLock::new(|| RwLock::new(Vec::with_capacity(1010))); 34 | 35 | #[tokio::main] 36 | async fn main() -> Result<(), Box> { 37 | let articles = list_articles()?; 38 | ARTICLES.write().await.extend(articles.clone()); // Set the articles in the ARTICLES static variable 39 | let out = Path::new("./out/blog"); 40 | if !out.exists() { 41 | std::fs::create_dir_all(out).expect("Cannot create 'out' directory"); 42 | } 43 | 44 | copy_dir_all("./assets", "./out/blog/assets").expect("Cannot copy 'assets' folder"); 45 | 46 | let ssg = Ssg::new(out); 47 | 48 | // generate the pages 49 | ( 50 | generate_homepage(&ssg), 51 | generate_post_pages(articles.clone(), &ssg), 52 | generate_pages(articles.clone(), &ssg), 53 | generate_esta_semana_en_rust(articles.clone(), &ssg), 54 | generate_tag_pages(articles.clone(), &ssg), 55 | ) 56 | .try_join() 57 | .await?; 58 | 59 | Ok(()) 60 | } 61 | 62 | fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { 63 | fs::create_dir_all(&dst)?; 64 | for entry in fs::read_dir(src)? { 65 | let entry = entry?; 66 | let ty = entry.file_type()?; 67 | if ty.is_dir() { 68 | copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; 69 | } else { 70 | fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; 71 | } 72 | } 73 | Ok(()) 74 | } 75 | 76 | async fn generate_homepage(ssg: &Ssg<'_>) -> Result<(), Box> { 77 | ssg.gen("index.html".to_owned(), || { 78 | Homepage(HomepageProps { 79 | articles: None, 80 | show_featured: true, 81 | page: None, 82 | max_page: 0, 83 | }) 84 | }) 85 | .await?; 86 | 87 | Ok(()) 88 | } 89 | 90 | async fn generate_esta_semana_en_rust( 91 | articles: Vec
, 92 | ssg: &Ssg<'_>, 93 | ) -> Result<(), Box> { 94 | let articles = articles 95 | .into_iter() 96 | .filter(|article| article.number_of_week.is_some()) 97 | .collect::>(); 98 | 99 | generate_feed_rss( 100 | &articles, 101 | "./out/blog/this_week_feed.xml", 102 | "Esta Semana en Rust", 103 | "Revisa que esta pasando en la comunidad de Rust Lang en Español", 104 | Some("tags/esta-semana-en-rust.html"), 105 | ); 106 | 107 | let generate_articles = articles.into_iter().map(|article| { 108 | if article.number_of_week.is_some() { 109 | ssg.gen(format!("articles/{}.html", article.slug), || { 110 | EstaSemanaEnRust(EstaSemanaEnRustProps { article }) 111 | }) 112 | } else { 113 | panic!("articles without number_of_week should be generated in generate_post_pages") 114 | } 115 | }); 116 | 117 | join_all(generate_articles).await; 118 | 119 | Ok(()) 120 | } 121 | 122 | async fn generate_post_pages( 123 | articles: Vec
, 124 | ssg: &Ssg<'_>, 125 | ) -> Result<(), Box> { 126 | tokio::fs::create_dir_all("./out/blog/articles").await?; 127 | 128 | let articles = articles 129 | .clone() 130 | .into_iter() 131 | .filter(|a| a.number_of_week.is_none()) 132 | .collect::>(); 133 | 134 | generate_feed_rss( 135 | &articles, 136 | "./out/blog/feed.xml", 137 | "Blog de RustLangES", 138 | "Enterate del mejor contenido en Español sobre Rust", 139 | None, 140 | ); 141 | 142 | let generate_articles = articles.into_iter().map(|article| { 143 | if article.number_of_week.is_none() { 144 | ssg.gen(format!("articles/{}.html", article.slug), move || { 145 | ArticlePage(ArticlePageProps { article }) 146 | }) 147 | } else { 148 | panic!( 149 | "articles with number_of_week should be generated in generate_esta_semana_en_rust" 150 | ) 151 | } 152 | }); 153 | 154 | join_all(generate_articles).await; 155 | 156 | Ok(()) 157 | } 158 | 159 | async fn generate_tag_pages( 160 | articles: Vec
, 161 | ssg: &Ssg<'_>, 162 | ) -> Result<(), Box> { 163 | tokio::fs::create_dir_all("./out/blog/tags").await?; 164 | 165 | let tags = articles 166 | .iter() 167 | .filter_map(|article| article.tags.clone()) 168 | .flatten() 169 | .map(|tag| { 170 | let articles_to_show = articles 171 | .clone() 172 | .into_iter() 173 | .filter(|article| { 174 | if let Some(tags) = article.tags.clone() { 175 | tags.contains(&tag) 176 | } else { 177 | false 178 | } 179 | }) 180 | .collect::>(); 181 | 182 | let tag = tag.to_lowercase().replace(' ', "-"); 183 | 184 | ssg.gen(format!("tags/{tag}.html"), || { 185 | Homepage(HomepageProps { 186 | articles: Some(articles_to_show), 187 | show_featured: false, 188 | page: None, 189 | max_page: 0, 190 | }) 191 | }) 192 | }); 193 | 194 | join_all(tags).await; 195 | 196 | Ok(()) 197 | } 198 | 199 | fn list_articles() -> Result, Box> { 200 | let mut articles = Vec::with_capacity(10); 201 | let article_folder = fs::read_dir("./articles")?; 202 | articles.append(&mut posts_from_folder(article_folder)?); 203 | 204 | let esta_semana_en_rust_folder = fs::read_dir("./esta_semana_en_rust")?; 205 | articles.append(&mut posts_from_folder(esta_semana_en_rust_folder)?); 206 | 207 | articles.sort_by(|a, b| b.date.cmp(&a.date)); 208 | 209 | Ok(articles) 210 | } 211 | 212 | fn posts_from_folder(paths: ReadDir) -> Result, Box> { 213 | let mut articles = Vec::with_capacity(10); 214 | 215 | for path in paths { 216 | let file = path?.path(); 217 | let algo = fs::read_to_string(file.clone())?; 218 | let matter = Matter::::new(); 219 | let Some(parsed_entity) = matter.parse_with_struct(&algo) else { 220 | println!("Error parsing file: {}", file.display()); 221 | continue; 222 | }; 223 | let content = parsed_entity.content.clone(); 224 | let mut article: Article = parsed_entity.data; 225 | article.content = content; 226 | if article.slug.is_empty() { 227 | // path without extension 228 | let filename_without_extension = file.file_stem().unwrap().to_str().unwrap(); 229 | article.slug = filename_without_extension.to_string(); 230 | } 231 | if article.date_string.is_none() { 232 | article.date_string = Some( 233 | article 234 | .date 235 | .format_localized("%e de %B del %Y", chrono::Locale::es_ES) 236 | .to_string(), 237 | ); 238 | } 239 | articles.push(article); 240 | } 241 | Ok(articles) 242 | } 243 | 244 | async fn generate_pages( 245 | mut articles: Vec
, 246 | ssg: &Ssg<'_>, 247 | ) -> Result<(), Box> { 248 | tokio::fs::create_dir_all("./out/blog/pages").await?; 249 | 250 | if let Some(last_this_week_in_rust) = articles.iter().position(|a| a.number_of_week.is_some()) { 251 | articles.remove(last_this_week_in_rust); 252 | } 253 | if let Some(announce_position) = articles.iter().position(|a| { 254 | a.tags 255 | .as_ref() 256 | .unwrap() 257 | .contains(&"anuncio de la comunidad".to_string()) 258 | }) { 259 | articles.remove(announce_position); 260 | } 261 | let max_pages = articles.len() / 6; 262 | let mut articles = articles.chunks(6).collect::>(); 263 | articles.remove(0); 264 | let articles = articles.clone(); 265 | 266 | let generate_articles = articles 267 | .iter() 268 | .enumerate() 269 | .map(|(index, articles_to_show)| { 270 | let articles_to_show = articles_to_show.to_vec(); 271 | ssg.gen(format!("pages/{}.html", index + 1), move || { 272 | Homepage(HomepageProps { 273 | articles: Some(articles_to_show), 274 | show_featured: false, 275 | page: Some(index + 1), 276 | max_page: max_pages, 277 | }) 278 | }) 279 | }); 280 | 281 | join_all(generate_articles).await; 282 | 283 | Ok(()) 284 | } 285 | -------------------------------------------------------------------------------- /input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* vietnamese */ 6 | @font-face { 7 | font-family: "Alfa Slab One"; 8 | font-style: normal; 9 | font-weight: 400; 10 | font-display: swap; 11 | src: url(https://fonts.gstatic.com/s/alfaslabone/v17/6NUQ8FmMKwSEKjnm5-4v-4Jh2d1he-Wv.woff2) 12 | format("woff2"); 13 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, 14 | U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, 15 | U+1EA0-1EF9, U+20AB; 16 | } 17 | /* latin-ext */ 18 | @font-face { 19 | font-family: "Alfa Slab One"; 20 | font-style: normal; 21 | font-weight: 400; 22 | font-display: swap; 23 | src: url(https://fonts.gstatic.com/s/alfaslabone/v17/6NUQ8FmMKwSEKjnm5-4v-4Jh2dxhe-Wv.woff2) 24 | format("woff2"); 25 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, 26 | U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 27 | } 28 | /* latin */ 29 | @font-face { 30 | font-family: "Alfa Slab One"; 31 | font-style: normal; 32 | font-weight: 400; 33 | font-display: swap; 34 | src: url(https://fonts.gstatic.com/s/alfaslabone/v17/6NUQ8FmMKwSEKjnm5-4v-4Jh2dJhew.woff2) 35 | format("woff2"); 36 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 37 | U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, 38 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 39 | } 40 | 41 | /* cyrillic-ext */ 42 | @font-face { 43 | font-family: "Fira Sans"; 44 | font-style: normal; 45 | font-weight: 400; 46 | font-display: swap; 47 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5VvmojLeTY.woff2) 48 | format("woff2"); 49 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, 50 | U+FE2E-FE2F; 51 | } 52 | /* cyrillic */ 53 | @font-face { 54 | font-family: "Fira Sans"; 55 | font-style: normal; 56 | font-weight: 400; 57 | font-display: swap; 58 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2) 59 | format("woff2"); 60 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 61 | } 62 | /* greek-ext */ 63 | @font-face { 64 | font-family: "Fira Sans"; 65 | font-style: normal; 66 | font-weight: 400; 67 | font-display: swap; 68 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2) 69 | format("woff2"); 70 | unicode-range: U+1F00-1FFF; 71 | } 72 | /* greek */ 73 | @font-face { 74 | font-family: "Fira Sans"; 75 | font-style: normal; 76 | font-weight: 400; 77 | font-display: swap; 78 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2) 79 | format("woff2"); 80 | unicode-range: U+0370-03FF; 81 | } 82 | /* vietnamese */ 83 | @font-face { 84 | font-family: "Fira Sans"; 85 | font-style: normal; 86 | font-weight: 400; 87 | font-display: swap; 88 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2) 89 | format("woff2"); 90 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, 91 | U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, 92 | U+1EA0-1EF9, U+20AB; 93 | } 94 | /* latin-ext */ 95 | @font-face { 96 | font-family: "Fira Sans"; 97 | font-style: normal; 98 | font-weight: 400; 99 | font-display: swap; 100 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2) 101 | format("woff2"); 102 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, 103 | U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 104 | } 105 | /* latin */ 106 | @font-face { 107 | font-family: "Fira Sans"; 108 | font-style: normal; 109 | font-weight: 400; 110 | font-display: swap; 111 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5Vvl4jL.woff2) 112 | format("woff2"); 113 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 114 | U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, 115 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 116 | } 117 | /* cyrillic-ext */ 118 | @font-face { 119 | font-family: "Fira Sans"; 120 | font-style: normal; 121 | font-weight: 500; 122 | font-display: swap; 123 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveSxf6TF0.woff2) 124 | format("woff2"); 125 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, 126 | U+FE2E-FE2F; 127 | } 128 | /* cyrillic */ 129 | @font-face { 130 | font-family: "Fira Sans"; 131 | font-style: normal; 132 | font-weight: 500; 133 | font-display: swap; 134 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveQhf6TF0.woff2) 135 | format("woff2"); 136 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 137 | } 138 | /* greek-ext */ 139 | @font-face { 140 | font-family: "Fira Sans"; 141 | font-style: normal; 142 | font-weight: 500; 143 | font-display: swap; 144 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveShf6TF0.woff2) 145 | format("woff2"); 146 | unicode-range: U+1F00-1FFF; 147 | } 148 | /* greek */ 149 | @font-face { 150 | font-family: "Fira Sans"; 151 | font-style: normal; 152 | font-weight: 500; 153 | font-display: swap; 154 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveRRf6TF0.woff2) 155 | format("woff2"); 156 | unicode-range: U+0370-03FF; 157 | } 158 | /* vietnamese */ 159 | @font-face { 160 | font-family: "Fira Sans"; 161 | font-style: normal; 162 | font-weight: 500; 163 | font-display: swap; 164 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveSRf6TF0.woff2) 165 | format("woff2"); 166 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, 167 | U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, 168 | U+1EA0-1EF9, U+20AB; 169 | } 170 | /* latin-ext */ 171 | @font-face { 172 | font-family: "Fira Sans"; 173 | font-style: normal; 174 | font-weight: 500; 175 | font-display: swap; 176 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveSBf6TF0.woff2) 177 | format("woff2"); 178 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, 179 | U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 180 | } 181 | /* latin */ 182 | @font-face { 183 | font-family: "Fira Sans"; 184 | font-style: normal; 185 | font-weight: 500; 186 | font-display: swap; 187 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnZKveRhf6.woff2) 188 | format("woff2"); 189 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 190 | U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, 191 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 192 | } 193 | /* cyrillic-ext */ 194 | @font-face { 195 | font-family: "Fira Sans"; 196 | font-style: normal; 197 | font-weight: 600; 198 | font-display: swap; 199 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeSxf6TF0.woff2) 200 | format("woff2"); 201 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, 202 | U+FE2E-FE2F; 203 | } 204 | /* cyrillic */ 205 | @font-face { 206 | font-family: "Fira Sans"; 207 | font-style: normal; 208 | font-weight: 600; 209 | font-display: swap; 210 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeQhf6TF0.woff2) 211 | format("woff2"); 212 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 213 | } 214 | /* greek-ext */ 215 | @font-face { 216 | font-family: "Fira Sans"; 217 | font-style: normal; 218 | font-weight: 600; 219 | font-display: swap; 220 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeShf6TF0.woff2) 221 | format("woff2"); 222 | unicode-range: U+1F00-1FFF; 223 | } 224 | /* greek */ 225 | @font-face { 226 | font-family: "Fira Sans"; 227 | font-style: normal; 228 | font-weight: 600; 229 | font-display: swap; 230 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeRRf6TF0.woff2) 231 | format("woff2"); 232 | unicode-range: U+0370-03FF; 233 | } 234 | /* vietnamese */ 235 | @font-face { 236 | font-family: "Fira Sans"; 237 | font-style: normal; 238 | font-weight: 600; 239 | font-display: swap; 240 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeSRf6TF0.woff2) 241 | format("woff2"); 242 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, 243 | U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, 244 | U+1EA0-1EF9, U+20AB; 245 | } 246 | /* latin-ext */ 247 | @font-face { 248 | font-family: "Fira Sans"; 249 | font-style: normal; 250 | font-weight: 600; 251 | font-display: swap; 252 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeSBf6TF0.woff2) 253 | format("woff2"); 254 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, 255 | U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 256 | } 257 | /* latin */ 258 | @font-face { 259 | font-family: "Fira Sans"; 260 | font-style: normal; 261 | font-weight: 600; 262 | font-display: swap; 263 | src: url(https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnSKzeRhf6.woff2) 264 | format("woff2"); 265 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 266 | U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, 267 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 268 | } 269 | 270 | .custom-shape-divider-top-1692767000 { 271 | width: 100%; 272 | overflow: hidden; 273 | line-height: 0; 274 | transform: rotate(180deg); 275 | } 276 | 277 | .custom-shape-divider-top-1692767000 svg { 278 | position: relative; 279 | display: block; 280 | width: calc(100% + 1.3px); 281 | height: 150px; 282 | /* TODO: fix the border made it with drop-shadow */ 283 | filter: drop-shadow(5px 5px 0px #ffffff); 284 | } 285 | 286 | .markdown-container { 287 | @apply flex flex-col; 288 | } 289 | 290 | .prose > *, 291 | .prose div > * { 292 | @apply my-2; 293 | } 294 | 295 | .prose ul > li > p, 296 | .prose ol > li > p { 297 | @apply my-0; 298 | } 299 | 300 | .markdown-container pre { 301 | box-shadow: 0 0 3px 0px black; 302 | width: 100%; 303 | overflow-x: auto; 304 | @apply max-w-[17rem] xs:max-w-md md:max-w-2xl lg:max-w-5xl; 305 | } 306 | .markdown-container pre code { 307 | width: 100%; 308 | } 309 | 310 | .highlight__panel.js-actions-panel { 311 | @apply hidden; 312 | } 313 | 314 | #sidebar { 315 | animation: ScrollAnimation linear; 316 | animation-timeline: scroll(root block); 317 | animation-range: 0 170px; 318 | animation-timing-function: linear; 319 | animation-fill-mode: both; 320 | } 321 | 322 | @keyframes ScrollAnimation { 323 | 0% { 324 | transform: translateY(0); 325 | } 326 | 100% { 327 | transform: translateY(30%); 328 | } 329 | } 330 | 331 | /** 332 | * Override prose styles for links inside headings 333 | */ 334 | .prose 335 | :where(h1 > a):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 336 | @apply no-underline font-bold hover:underline; 337 | } 338 | 339 | .prose 340 | :where(h2 > a):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 341 | @apply no-underline font-bold hover:underline; 342 | } 343 | 344 | .prose 345 | :where(h3 > a):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 346 | @apply no-underline font-semibold hover:underline; 347 | } 348 | 349 | .prose 350 | :where(h4 > a):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 351 | @apply no-underline font-semibold hover:underline; 352 | } 353 | 354 | .prose 355 | :where(h5 > a):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 356 | @apply no-underline font-semibold hover:underline; 357 | } 358 | 359 | .prose 360 | :where(h6 > a):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 361 | @apply no-underline font-semibold hover:underline; 362 | } 363 | 364 | .language-tag { 365 | @apply sr-only; 366 | } 367 | 368 | /** 369 | * Custom code block styles 370 | */ 371 | .hh0 { 372 | color: #ffd173; /* attribute */ 373 | } 374 | 375 | .hh1 { 376 | color: #d5ff80; /* constant */ 377 | } 378 | 379 | .hh2 { 380 | color: #dfbfff; /* built-in function */ 381 | } 382 | 383 | .hh3 { 384 | color: #ffd173; /* function */ 385 | } 386 | 387 | .hh4 { 388 | color: #ffad66; /* keyword */ 389 | } 390 | 391 | .hh5 { 392 | color: #f29e74; /* operator */ 393 | } 394 | 395 | .hh6 { 396 | color: #5ccfe6; /* property */ 397 | } 398 | 399 | .hh7 { 400 | color: #cccac2; /* punctuation */ 401 | } 402 | 403 | .hh8 { 404 | color: #8a9199b3; /* bracket punctuation */ 405 | } 406 | 407 | .hh9 { 408 | color: #cccac2b3; /* delimiter punctuation */ 409 | } 410 | 411 | .hh10 { 412 | color: #d5ff80; /* string */ 413 | } 414 | 415 | .hh11 { 416 | color: #95e6cb; /* special string */ 417 | } 418 | 419 | .hh12 { 420 | color: #5ccfe6; /* tag */ 421 | } 422 | 423 | .hh13 { 424 | color: #f29e74; /* type */ 425 | } 426 | 427 | .hh14 { 428 | color: #dfbfff; /* built-in type */ 429 | } 430 | 431 | .hh15 { 432 | color: #f29e74; /* variable */ 433 | } 434 | 435 | .hh16 { 436 | color: #dfbfff; /* built-in variable */ 437 | } 438 | 439 | .hh17 { 440 | color: #f29e74; /* parameter variable */ 441 | } 442 | 443 | .hh18 { 444 | color: #5ccfe6; /* comment */ 445 | } 446 | 447 | .hh19 { 448 | color: #f29e74; /* macro */ 449 | } 450 | 451 | .hh20 { 452 | color: #f29e74; /* label */ 453 | } 454 | 455 | /* 456 | * Navigation styles 457 | */ 458 | .nav-item { 459 | cursor: pointer; 460 | } 461 | 462 | .nav-item:after { 463 | content: ''; 464 | @apply block border border-orange-600 transition-transform ease-in-out duration-200 scale-x-0; 465 | } 466 | 467 | .nav-item:hover:after { 468 | @apply scale-x-100; 469 | } 470 | -------------------------------------------------------------------------------- /articles/un-pequeno-paseo-por-rust-4lko.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Un pequeño paseo por Rust 3 | description: Antes de comenzar, quiero aclarar que no es para nada sencillo escribir casi a diario. Cada vez que... 4 | author: Maximiliano Burgos 5 | github_user: maxwellnewage 6 | date: 2023-05-27 7 | tags: 8 | - rust 9 | - comunidad 10 | - devjournal 11 | - devto 12 | social: 13 | github: https://github.com/maxwellnewage 14 | twitter: https://twitter.com/maxwellnewage 15 | linkedin: https://www.linkedin.com/in/maximilianoburgos/ 16 | --- 17 | Antes de comenzar, quiero aclarar que no es para nada sencillo escribir casi a diario. Cada vez que lo intento, termino inmerso en una nueva investigación que me lleva semanas, o incluso un mes entero. Este fue el caso de Rust. 18 | 19 | # ¿Pero cómo llegué acá? 20 | 21 | Es una pregunta que me hago probablemente desde mi nacimiento, pero en este caso fue un poco más extraño: si leyeron [mi artículo anterior](https://dev.to/maxwellnewage/creo-que-me-converti-en-un-desarrollador-en-react-y-typescript-2fkf), mi plan en muy resumidas cuentas, era aprender React y TypeScript. Mi felicidad estaba llegando, dado que terminaría mi fase de estudiante eterno para introducirme finalmente en el momento de empezar el bendito proyecto del clicker, hasta que llegó esto: 22 | 23 |
24 | 25 |
26 | 27 | Tauri es un duro competidor de ElectronJS, y da bastante miedo: he visto varias comparativas y supera con creces a éste último. El punto más fuerte de Tauri es que compila en código de máquina; no como Electron que hace una especie de paquete con NodeJS y arma un ejecutable híbrido, además de ensuciar bastante el proyecto. Pueden encontrar mis repositorios con el clicker implementado en [Electron](https://github.com/maxwellnewage/react-clicker) y [Tauri](https://github.com/maxwellnewage/tauri-clicker). 28 | 29 | Encontré en Tauri una solución más limpia y robusta, pero existía un pequeño (gran) detalle: estaba desarrollada en Rust. 30 | 31 | # Otro viaje de aprendizaje, el último, espero. 32 | 33 | En el mundo del desarrollo, Rust es sinónimo de un "señor lenguaje de programación". La gente lo adora, especialmente en las vastas tierras de StackOverflow. Siempre me dió cierta curiosidad, pero en esas épocas mi objetivo era especializarme en Android y Kotlin. 34 | 35 | Si bien se puede desarrollar una aplicación web con Tauri, y dejar que sus componentes hagan su trabajo como una caja negra; mi necesidad de saberlo todo no me dejaba en paz: tenía que conocer Rust, de la misma forma que todos los conductores deberíamos saber mecánica para entender cómo funciona un vehículo por dentro. 36 | 37 | Como siempre, me dediqué a buscar artículos y videos de youtube. Dejo una lista de aquellos que me parecieron más interesantes: 38 | 39 | - [Por qué tienes que aprender Rust](https://www.youtube.com/watch?v=-anm93UDkTk) 40 | 41 | - [Rust esta mejorando el desarrollo web](https://www.youtube.com/watch?v=-anm93UDkTk) 42 | 43 | - [Rust 101 Crash Course](https://www.youtube.com/watch?v=lzKeecy4OmQ): Este recurso en particular me encantó, nunca vi alguien que explicara tan bien los conceptos. 44 | 45 | - [¿Salvará Rust el mundo? Parte 1](https://empresas.blogthinkbig.com/rust-errores-programacion-uso-memoria/): En el final del artículo esta el enlace a la segunda parte. 46 | 47 | - [¿Qué es Rust?](https://www.youtube.com/shorts/QPhbR7oIOq8) 48 | 49 | Luego de estudiar un par de días, me di cuenta que Rust era extremadamente difícil. 50 | 51 | # Rust es difícil 52 | 53 | Me encontré en una situación compleja: Rust se me antojaba esotérico a ratos, con una sintaxis muy propia del lenguaje que no había visto desde que dejé de programar en C o C++. Por ejemplo, esto es un "Hello World" con variables: 54 | 55 | ```rust 56 | fn prints() { 57 | let mut name: &str = "Tomas"; 58 | println!("Hello, {}!", name); 59 | name = "Max"; 60 | println!("Hello, {}!", name); 61 | } 62 | ``` 63 | 64 | En este caso, "let mut" define a _name_ como una variable mutable, y "&str" es algo parecido a un tipo String. Luego, "println!" tiene un signo de admiración porque se trata de una macro (la cual es distinta y parecida a una función, al mismo tiempo) y debemos utilizar llaves para representar valores por cómo funcionan los tipos. 65 | 66 | Vengo de lenguajes de tipado dinámico (salvando quizá, Kotlin y Java), por lo cual encontrarme con lo que vamos a llamar "tipado muy fuerte", que al mínimo pasaje incorrecto deja de compilar, fue una pesadilla inicialmente. 67 | 68 | La curva continuó subiendo a medida que aprendí sobre otros conceptos, como operadores: 69 | 70 | ```rust 71 | fn ops() { 72 | // Addition, Subtraction, and Multiplication 73 | println!( 74 | "1 + 2 = {} and 8 - 5 = {} and 15 * 3 = {}", 75 | 1 + 2, 76 | 8 - 5, 77 | 15 * 3 78 | ); 79 | // Integer and Floating point division 80 | println!("9 / 2 = {} but 9.0 / 2.0 = {}", 9 / 2, 9.0 / 2.0); 81 | println!("9 / 2 = {}", 9f32 / 2f32); 82 | } 83 | ``` 84 | 85 | Donde una división de enteros no podía generar un flotante, entonces necesitabas definirlo previamente (9 flotante de 32 bits). 86 | 87 | Por otro lado, las tuplas eran similares a Python, pero muy extrañas en su definición: 88 | 89 | ```rust 90 | fn tupla() { 91 | // Tuple of length 3 92 | let tuple_e: (char, i32, bool) = ('E', 5i32, true); 93 | 94 | // Use tuple indexing and show the values of the elements in the tuple 95 | println!( 96 | "Is '{}' the {}th letter of the alphabet? {}", 97 | tuple_e.0, tuple_e.1, tuple_e.2 98 | ); 99 | } 100 | 101 | ``` 102 | 103 | El tipo "char" implica un valor de un solo caracter; mientras que "i32" implica un entero sin signo de 32 bits. 104 | 105 | Estaba claro que Rust era un lenguaje de bajo nivel que tenía características que comparten otros lenguajes más contemporaneos. Esto me resultaba preocupante, dado que no manejaba este nivel de complejidad. 106 | 107 | Por otro lado, **Rust no maneja el paradigma orientado a objetos**. Esto me dejó un poco atontado, porque venía trabajando con esta forma en todos los lenguajes que me he cruzado. Aquí estamos frente al uso de los paradigmas imperativo y funcional. Si queremos algo similar a los objetos, necesitamos utilizar cosas como struct e impl: 108 | 109 | ```rust 110 | 111 | enum Color { 112 | Brown, 113 | Red, 114 | } 115 | 116 | impl Color { 117 | 118 | fn print(&self) { 119 | match self { 120 | Color::Brown =\u003E println!("brown"), 121 | Color::Red =\u003E println!("red") 122 | } 123 | } 124 | 125 | } 126 | 127 | struct Dimensions { 128 | width: f64, 129 | height: f64, 130 | depth: f64, 131 | } 132 | 133 | impl Dimensions { 134 | fn print(&self) { 135 | println!("width: {:?}", self.width); 136 | println!("height: {:?}", self.height); 137 | println!("depth: {:?}", self.depth); 138 | } 139 | } 140 | 141 | struct ShippingBox { 142 | color: Color, 143 | weight: f64, 144 | dimensions: Dimensions, 145 | } 146 | 147 | impl ShippingBox { 148 | fn new(weight: f64, color: Color, dimensions: Dimensions) -\u003E Self { 149 | Self { 150 | weight, 151 | color, 152 | dimensions, 153 | } 154 | } 155 | 156 | fn print(&self) { 157 | self.color.print(); 158 | self.dimensions.print(); 159 | println!("weight: {:?}", self.weight); 160 | } 161 | } 162 | 163 | fn main() { 164 | let small_dimensions = Dimensions { 165 | width: 1.0, 166 | height: 2.0, 167 | depth: 3.0 168 | }; 169 | 170 | let small_box = ShippingBox::new(5.0, Color::Red, small_dimensions); 171 | 172 | small_box.print(); 173 | } 174 | 175 | ``` 176 | 177 | En este ejemplo, _ShippingBox_ es una estructura de datos que posee su propia implementación, así como también pasa con _Color_. Es probable que los que estén más familiarizados con C, encuentren esto muy razonable; pero yo me crié (laboralmente) con lenguajes que aplicaban la POO, conjunto de sus patrones de diseño y buenas prácticas. 178 | 179 | # Finalmente descubrí donde estaba el amor. 180 | 181 | Si bien mi subtítulo es aplicable al título de una novela rosa, o un relato de Wattpad; la realidad es que finalmente encontré la clave de tanto amor a Rust: El concepto de Borrow, o "prestar". 182 | 183 | A diferencia de C/C++, donde teníamos que manejar la memoria a mano, Rust nos ofrece un sistema inteligente de préstamo de recursos. Por ejemplo: 184 | 185 | ```rust 186 | 187 | fn main() { 188 | let nums = vec![10, 20, 30, 40]; 189 | 190 | for n in nums { 191 | match n { 192 | 30 =\u003E println!("thirty"), 193 | _ =\u003E println!("{:?}", n), 194 | } 195 | } 196 | 197 | println!("Number of elements: {:?}", nums.len()); 198 | } 199 | 200 | ``` 201 | 202 | Aquí podemos ver cómo recorro un vector de números mediante un bucle for. En un lenguaje tradicional, pedir el número de elementos luego de recorrerlo no sería un problema; pero en nuestro caso va a fallar en la compilación. 203 | 204 | Esto es porque la variable _nums_, luego de ser procesada en un bucle (o pasada por una función), se destruye. Por supuesto, en este caso deberíamos evitarlo, y lo hacemos mediante el símbolo "&": 205 | 206 | ```rust 207 | 208 | for n in &nums { 209 | 210 | ``` 211 | 212 | Con este pequeño cambio, estamos pidiéndole a la función main (actual dueño de nuestro vector nums) que **preste** al bucle _for_ dicho vector. Ésta técnica podría compararse con un pasaje de datos por referencia. 213 | 214 | Cuando nuestro bucle termina de utilizar su vector, lo devuelve a main() y se continúa su ejecución. Con este sistema de borrowing, podemos liberar memoria cuando realmente no necesitemos los recursos. Esto claramente es un digno competidor de los punteros en C; y supera con creces el Garbage Collector de lenguajes como Python, Java, C#, entre otros. 215 | 216 | **Éste** es el verdadero potencial de Rust. Y no, no somos dignos de esta maravillosa tecnología. 217 | 218 | # El trayecto final: La implementación. 219 | 220 | Con el fin de aplicar todo lo que aprendí, me propuse armar un proyecto sencillo y que ya esta publicado en GitHub: [Rust Hero Game](https://github.com/maxwellnewage/rust-hero-game). Este proyecto utiliza Rust para acceder a la API de [Hero Game](https://github.com/maxwellnewage/udemy-django-hero-game) desarrollada en Django y DRF. 221 | 222 | En primer lugar, definí los recursos que iba a utilizar: tenía una API, y el endpoint más sencillo era "/api/players/", un GET que obtenía los jugadores sin pedir autenticación: 223 | 224 | ```json 225 | [ 226 | { 227 | "id": 1, 228 | "name": "maxwell", 229 | "hp": 50, 230 | "money": 99, 231 | "score": 5, 232 | "owner": { 233 | "id": 1, 234 | "username": "admin", 235 | "is_author": true 236 | } 237 | } 238 | ] 239 | ``` 240 | 241 | Encendí el servidor y me puse a trabajar en un archivo llamado api.rs: 242 | 243 | ```rust 244 | const BASE_URL: &str = "http://127.0.0.1:8001/api/"; 245 | async fn make_api_request(url: &str) -\u003E Result\u003Cserde_json::Value, Error\u003E { 246 | let resp = reqwest::get(url) 247 | .await? 248 | .json::\u003Cserde_json::Value\u003E() 249 | .await?; 250 | Ok(resp) 251 | } 252 | ``` 253 | 254 | Definí una constante BASE_URL, la cual se explica por sí misma, y armé un método _make_api_request_, el cual toma un endpoint y devuelve un enum Result que se lleva serde_json::Value (si la cosa fue bien) y Error (si salió algo mal). 255 | 256 | Utiliza async y await para detener los procesos en cada paso, pero seguir trabajando en los eventos asincrónicos como el llamado a la API y la conversión a json. 257 | 258 | ```rust 259 | pub async fn get_all_players() -\u003E Result\u003CVec\u003CPlayer\u003E, ApiError\u003E { 260 | let url = &format!("{}{}", BASE_URL, "players/"); 261 | match make_api_request(url).await { 262 | Ok(resp) =\u003E { 263 | match serde_json::from_value::\u003CVec\u003CPlayer\u003E\u003E(resp) { 264 | Ok(players) =\u003E Ok(players), 265 | Err(e) =\u003E Err(ApiError::from(e)), 266 | } 267 | } 268 | Err(e) =\u003E Err(ApiError::from(e)), 269 | } 270 | } 271 | ``` 272 | 273 | Luego, mi función _get_all_players_ llama a _make_api_request_, pero procesa la respuesta para serializar el json en un struct Player: 274 | 275 | ```rust 276 | pub struct Player { 277 | hp: i32, 278 | id: i32, 279 | money: i32, 280 | name: String, 281 | score: i32, 282 | owner: Owner 283 | } 284 | ``` 285 | 286 | De esta manera, en main podemos trabajar con cada atributo como si fuera un "objeto": 287 | 288 | ```rust 289 | async fn main() { 290 | match api::get_all_players().await { 291 | Ok(players) =\u003E { 292 | for player in players { 293 | println!("Jugador: {:?}", player); 294 | } 295 | } 296 | Err(e) =\u003E { 297 | eprintln!("Error al obtener los jugadores: {}", e); 298 | } 299 | } 300 | } 301 | ``` 302 | 303 | Dentro de cada player del for (devuelve un array de players en json, vector en Rust) podríamos acceder a propiedades como el dinero mediante _player.money_. 304 | 305 | Si corremos por consola el programa, obtendremos un resultado similar a este: 306 | 307 | ``` 308 | Finished dev [unoptimized + debuginfo] target(s) in 0.82s 309 | Running `target\\debug\\hero-game.exe` 310 | Jugador: Player { hp: 50, id: 1, money: 99, name: "maxwell", score: 5, owner: Owner { id: 1, is_author: true, username: "admin" } } 311 | ``` 312 | 313 | # Conclusiones 314 | 315 | Creo que Rust es un excelente lenguaje de programación, creado bajo una muy buena idea acerca de cómo manejar los recursos en memoria. Al mismo tiempo, admito que no tengo las capacidades suficientes para dominarlo: puedo llevar un proyecto a cabo, pero he notado un grado de complejidad que me sobrepasa. 316 | 317 | Actualmente, no logro comprender en detalle los errores que lanza, y casi todo el tiempo me encuentro preguntándole a ChatGPT qué estoy haciendo. 318 | 319 | No obstante, esto también me sirve como una lección de humildad: todos los lenguajes que venía estudiando, los dominaba al mes. Hoy me cruzo con un gigante, y entiendo que parte de aprender se basa en la idea de admitir que no sabíamos todo; y que a veces hay que volver hacia atrás para tomar un envión más fuerte. 320 | 321 | Quiero aclarar que no tengo quejas con Rust, sino más bien observaciones comparables a mis otros aprendizajes. Y por supuesto, entrará en el stack que implica hacer este juego. 322 | 323 | En el próximo artículo, les contaré en detalle, a nivel mucho más técnico, sobre el clicker que me impulsó a aplicar Tauri y React. 324 | 325 | -------------------------------------------------------------------------------- /articles/strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: String en Rust 3 | description: El manejo de texto es algo muy importante en cualquier tipo de aplicación, por lo que conocer los tipos de datos que proporciona el lenguaje es muy importante, ademas de saber como poder manejar los datos de manera eficiente 4 | author: Sergio Ribera 5 | github_user: SergioRibera 6 | date: 2023-10-11 7 | tags: 8 | - rust 9 | - data-type 10 | - strings 11 | social: 12 | github: https://github.com/SergioRibera 13 | twitter: https://twitter.com/sergioribera_rs 14 | website: https://bento.me/sergioribera 15 | --- 16 | Primero tenemos que partir por la premisa de: 17 | 18 | ## Que es un String? 19 | Bueno un `String` en la mayoría de los lenguajes de programación, es una secuencia de caracteres o símbolos que se utiliza para representar texto o información legible por humanos en un programa. Estos caracteres pueden ser letras, números, símbolos especiales, espacios en blanco, emojis, etc. Los Strings son fundamentales en la programación, ya que se utilizan para manejar y manipular texto, desde mensajes en una aplicación hasta contenido web y datos de usuario. 20 | 21 | Los Strings suelen ser objetos o tipos de datos específicos en un lenguaje de programación y pueden tener diversas propiedades y características. Algunas de las características comunes de los Strings incluyen: 22 | 23 | 1. Inmutabilidad: En algunos lenguajes, los Strings son inmutables, lo que significa que una vez que se crea un String, no se puede modificar. En su lugar, cualquier operación que cambie el String crea uno nuevo. 24 | 25 | 2. Longitud: Los Strings tienen una longitud que representa la cantidad de caracteres que contienen. Puedes obtener la longitud de un String mediante una función o método proporcionado por el lenguaje. 26 | 27 | 3. Concatenación: Puedes combinar o unir Strings para crear uno nuevo más largo. Esto se llama concatenación y se utiliza para crear mensajes o texto compuesto. 28 | 29 | 4. Acceso a caracteres: Puedes acceder a caracteres individuales dentro de un String utilizando índices o posiciones. Algunos lenguajes comienzan a contar desde 0, lo que significa que el primer carácter tiene un índice 0, el segundo tiene un índice 1 y así sucesivamente. 30 | 31 | 5. Manipulación: Los Strings a menudo tienen métodos o funciones incorporados que permiten realizar diversas operaciones, como búsqueda y reemplazo de texto, conversión de mayúsculas a minúsculas y viceversa, y más. 32 | 33 | Para permitir la representación multilenguaje a nivel mundial existe un estándar de codificación de caracteres llamado Unicode y su variante UTF-8 (Unicode Transformation Format - 8 bits) las cuales permiten representar símbolos y caracteres de todos los idiomas del mundo 34 | 35 | Antes de ver los Strings en Rust, tenemos que ver los strings en otros lenguajes para poder apreciar el valor que propone Rust en sus implementaciones 36 | 37 | ## Cadenas de caracteres en C 38 | En C, las cadenas de caracteres son arreglos de caracteres que terminan con el carácter nulo '\0'. Este enfoque es propenso a errores, ya que no se realiza un seguimiento explícito de la longitud de la cadena, lo que puede llevar a desbordamientos de búfer (Buffer overflow) y problemas de seguridad. 39 | 40 | 1. No se Realiza un Seguimiento de la Longitud: A diferencia de los lenguajes de programación modernos, C no realiza un seguimiento automático de la longitud de las cadenas. Esto significa que debes usar funciones como `strlen()` para determinar la longitud de una cadena antes de manipularla. Si olvidas hacerlo o calculas incorrectamente la longitud, puedes introducir errores de acceso a memoria no válida. 41 | 42 | 2. No es Seguro contra Desbordamientos: La función `strcpy()` en C, que se utiliza para copiar una cadena en otra, no realiza comprobaciones de límites. Si la cadena de origen es más larga que la de destino, se producirá un desbordamiento de búfer (Buffer overflow), lo que puede ser explotado por un atacante. 43 | 44 | Aquí un pequeño ejemplo: 45 | 46 | ```c 47 | char destination[10]; 48 | char source[] = "Esta cadena es demasiado larga para el destino"; 49 | strcpy(destination, source); // Desbordamiento de búfer (Buffer overflow) 50 | ``` 51 | 52 | 3. Dificultades con Caracteres Multibyte: C no maneja naturalmente caracteres multibyte, lo que puede ser problemático en aplicaciones internacionales que requieren soporte para varios idiomas. La manipulación de caracteres multibyte puede ser propensa a errores y no es trivial en C. 53 | 54 | 4. Falta de Abstracciones de Alto Nivel: C carece de abstracciones de alto nivel para trabajar con cadenas de caracteres, como las que ofrecen lenguajes más modernos. Esto hace que sea más fácil cometer errores y dificulta la escritura de código seguro y legible. 55 | 56 | ## Cadenas de Caracteres en Rust 57 | Rust ofrece varios tipos de cadenas de caracteres que pueden adaptarse a diferentes necesidades, lo que hace que trabajar con texto sea seguro y eficiente. 58 | 59 | En Rust, las cadenas de caracteres son una colección de caracteres Unicode, lo que significa que pueden representar una amplia gama de idiomas y símbolos. A diferencia de otros lenguajes de programación, Rust ofrece varios tipos de cadenas de caracteres para abordar diferentes casos de uso: 60 | 61 | - &str: Este tipo representa una "sección" de una cadena de caracteres. Es una referencia a un segmento de memoria que contiene texto. Las cadenas &str son inmutables y se utilizan principalmente para referenciar datos de texto existentes y suele ser el tipo de dato por defecto cuando declaramos una cadena de texto explicita en el programa. 62 | ```rs 63 | let name: &str = "Sergio Ribera"; // Cadena de texto de tipo &'static str 64 | ``` 65 | 66 | - String: Este tipo representa una cadena de caracteres de propiedad (owned). Se trata de una cadena de texto que es propiedad exclusiva del programa. Puedes modificar una cadena String, añadir o eliminar caracteres, lo que la hace útil para construir y manipular cadenas de texto. Aunque parezca raro es mejor pensar en este tipo de dato como un `Vec` ya que es un arreglo de caracteres alojados en la memoria dinámica (Heap). 67 | ```rs 68 | let name: String = String::from("Sergio Ribera"); 69 | // El programa se encargará automáticamente de liberar la memoria utilizada por `name` cuando ya no sea necesaria, generalmente cuando la variable sale de ámbito. 70 | ``` 71 | 72 | - str: Es el tipo de dato más general y se utiliza para representar una referencia a una cadena de caracteres sin asignación específica. Es el tipo subyacente de &str y se utiliza raramente directamente en el código. 73 | 74 | > NOTA: Cuando se dice que una cadena de caracteres es de "propiedad" u "owned" en el contexto de Rust, significa que esa cadena está bajo el control exclusivo del programa y es responsable de su gestión de memoria. En otras palabras, la cadena de caracteres es propiedad del programa y se encargará de liberar automáticamente la memoria asignada a la cadena una vez que ya no sea necesaria. Esto es una parte fundamental del sistema de gestión de memoria de Rust y es una de las características clave que lo hacen seguro y eficiente. 75 | > En Rust, las cadenas de caracteres de propiedad se representan con el tipo de dato String. Cuando creas una cadena String, estás asignando y administrando explícitamente la memoria necesaria para almacenar la cadena y su contenido. Esto permite que el programa realice operaciones de modificación en la cadena, como agregar o quitar caracteres, sin correr riesgo de desbordamientos de búfer (Buffer overflow) o corrupción de memoria. 76 | 77 | ## 🐄 Cow (Clone On Write) 78 | El tipo Cow (en realidad `Cow<'a, B>`) en Rust es una estructura de datos que representa una cadena de caracteres y se utiliza para evitar copias innecesarias de datos al trabajar con cadenas. La abreviatura "Cow" significa "Clone on Write" (Clonar al escribir)", dependiendo de si la cadena se toma prestada o se clona según sea necesario de manera eficiente. 79 | 80 | > NOTA: En este articulo estamos hablando de las cadenas de texto en Rust, por lo que para nosotros el `Cow` en realidad sera `Cow<'a, str>`, en donde gestionará una referencia de str 81 | 82 | Definición: 83 | ```rs 84 | # mas informacion https://doc.rust-lang.org/std/borrow/enum.Cow.html 85 | pub enum Cow<'a, B> 86 | where 87 | B: 'a + ToOwned + ?Sized, 88 | { 89 | Borrowed(&'a B), 90 | Owned(::Owned), 91 | } 92 | ``` 93 | 94 | Como puedes notar, `Cow` es un enum que tiene dos variantes: `Borrowed` y `Owned`. 95 | - `Cow::Borrowed(&'a B)` se utiliza cuando se quiere trabajar con una referencia prestada a una cadena de caracteres existente. 96 | - `Cow::Owned(::Owned)` se utiliza cuando se necesita una copia de la cadena, y esta se clona. 97 | 98 | ```rs 99 | use std::borrow::Cow; 100 | 101 | let borrowed: Cow = Cow::Borrowed("Hello"); 102 | let owned: Cow = Cow::Owned(String::from("Hola")); 103 | 104 | let borrowed_ref: &str = &borrowed; 105 | let owned_string: String = owned.into_owned(); 106 | ``` 107 | 108 | ## Problemas con el String 109 | Si, aunque parezca raro leer que Rust pueda tener problemas con un tipo de dato, en realidad esto se refiere mas al mal uso que pueda existir, por eso te comento algunos problemas comunes que suele haber al respecto: 110 | 111 | 1. Consumo de Memoria: El tipo `String` en Rust es dinámico y crece automáticamente para acomodar el texto. Si no se administra cuidadosamente, esto puede llevar al consumo excesivo de memoria. Al crear y manipular múltiples Strings grandes, podrías agotar la memoria disponible. 112 | ```rs 113 | fn main() { 114 | let mut big_string = String::new(); 115 | for _ in 0..10000 { 116 | big_string.push_str("Texto grande "); // Crecimiento automático 117 | } 118 | } 119 | ``` 120 | 121 | 2. Copias Innecesarias: La copia de Strings puede ser costosa en términos de tiempo y memoria. Si copias una String cuando no es necesario, puedes incurrir en una sobrecarga de rendimiento. Por ejemplo, si clonas una cadena cuando podrías haber trabajado con una referencia prestada (&str), se realizará una copia innecesaria. 122 | ```rs 123 | fn main() { 124 | let original = "Texto original".to_string(); 125 | let copied = original.clone(); // Copia innecesaria 126 | } 127 | ``` 128 | 129 | 3. Fragmentación de la Memoria: La asignación y liberación frecuentes de Strings grandes pueden provocar fragmentación de la memoria. Esto puede afectar negativamente al rendimiento general del programa y al uso de la memoria. 130 | ```rs 131 | fn main() { 132 | let mut large_strings = Vec::new(); 133 | for _ in 0..1000 { 134 | let new_string = "Texto grande".to_string(); 135 | large_strings.push(new_string); // Asignación y liberación de new_string 136 | } 137 | } 138 | ``` 139 | 140 | 4. Textos innecesarios en el código: Si vienes de otros lenguajes muy probablemente sea una practica muy común, pero en Rust tenemos otros tipos de datos que quizás puedan ser mejor en ciertas situaciones. 141 | ```rs 142 | fn main() { 143 | let type_account: &str = "PERSONAL"; // creamos y almacenamos un str estatico 144 | match type_account { 145 | "PERSONAL" => todo!(), // creamos y almacenamos un str estatico para la comparacion 146 | "SHARED" => todo!(), // creamos y almacenamos un str estatico para la comparacion 147 | "BUSINESS" => todo!(), // creamos y almacenamos un str estatico para la comparacion 148 | _ => todo!(), 149 | } 150 | 151 | // Para este caso concreto lo ideal seria usar un enum, ya que ocupa mucha menos memoria y tiene mejores implementaciones para estos casos 152 | } 153 | ``` 154 | 155 | ## Dame soluciones no problemas 156 | Paso a paso, primero necesito que entiendas los problemas que pueden existir en la manipulación de textos. 157 | Ahora que ya viste los tipos de strings que maneja Rust y los problemas que pueden existir, veamos algunas estrategias para abordar estos problemas y optimizar el manejo de grandes cantidades de texto en Rust. 158 | 159 | - Usa Referencias (&str) cuando sea posible: Cuando no necesitas modificar una cadena, utiliza referencias a cadenas de caracteres (&str) en lugar de clonar (String). Esto evita copias innecesarias y reduce el consumo de memoria. 160 | ```rs 161 | fn process_text(text: &str) { 162 | println!("Procesando: {}", text); 163 | } 164 | 165 | fn main() { 166 | let large_text = "Este es un texto largo que no se clona".to_string(); 167 | process_text(&large_text); // Evita copiar la cadena 168 | } 169 | ``` 170 | 171 | - Utiliza Cow<'a, str>: Cow te permite trabajar con referencias prestadas o datos clonados según sea necesario, lo que puede ser útil al procesar texto dinámico. 172 | ```rs 173 | use std::borrow::Cow; 174 | 175 | fn process_text(text: Cow) { 176 | println!("Procesando: {}", text); 177 | } 178 | 179 | let borrowed_text: &str = "Texto prestado"; 180 | let owned_text: String = "Texto clonado".to_string(); 181 | 182 | process_text(Cow::Borrowed(borrowed_text)); // No se clona 183 | process_text(Cow::Owned(owned_text)); // Se clona si es necesario 184 | ``` 185 | 186 | - Usa la asignación cuidadosa de capacidad: Al crear Strings, puedes asignar una capacidad inicial para evitar asignaciones de memoria excesivas. Esto se hace utilizando el método .with_capacity(). 187 | ```rs 188 | let mut large_string = String::new(); 189 | large_string.reserve(1000); // Asigna capacidad inicial 190 | large_string.push_str("Texto largo..."); 191 | ``` 192 | 193 | - Recicla y reutiliza Strings: Si necesitas crear y desechar muchas Strings en un bucle, considera reutilizar Strings existentes para reducir la asignación de memoria. 194 | ```rs 195 | let mut reused_string = String::with_capacity(1000); 196 | for i in 1..100 { 197 | reused_string.clear(); // Reutiliza la misma cadena 198 | reused_string.push_str("Iteración "); 199 | reused_string.push(i.to_string()); 200 | println!("{}", reused_string); 201 | } 202 | ``` 203 | 204 | - Optimiza las operaciones de cadena: Al realizar operaciones de cadena, como concatenación, utiliza métodos que minimicen las copias, como push_str() o push() en lugar de + o format!(). 205 | ```rs 206 | let mut result = String::new(); 207 | for i in 1..1000 { 208 | result.push_str("Número: "); 209 | result.push(i.to_string().as_str()); // Minimiza copias 210 | result.push_str(", "); 211 | } 212 | ``` 213 | 214 | ## Conclusión 215 | En resumen, los Strings desempeñan un papel fundamental en la programación, ya que representan texto legible por humanos en aplicaciones y sistemas. En la mayoría de los lenguajes de programación, un String es una secuencia de caracteres que puede contener letras, números, símbolos especiales y otros elementos de texto. 216 | 217 | Sin embargo, el manejo de Strings puede presentar desafíos y problemas comunes en términos de consumo de memoria, copias innecesarias y fragmentación de la memoria. Estos problemas se hacen evidentes en lenguajes como C, donde el seguimiento de la longitud de las cadenas y la seguridad son preocupaciones principales. 218 | 219 | Rust aborda estos problemas ofreciendo tipos de cadenas de caracteres seguros y eficientes. El uso de referencias (&str) siempre que sea posible, junto con la estructura Cow<'a, str>, permite minimizar copias innecesarias y administrar eficazmente el consumo de memoria. Además, Rust proporciona métodos para asignar capacidad inicial y reutilizar cadenas, optimizando así las operaciones de cadena. 220 | 221 | En resumen, Rust ofrece soluciones efectivas para los problemas comunes relacionados con los Strings, lo que lo convierte en un lenguaje potente y seguro para el manejo de texto y cadenas de caracteres en aplicaciones modernas. La comprensión y aplicación de estas estrategias permiten a los desarrolladores aprovechar al máximo el potencial de Rust en el procesamiento y manipulación de texto de manera segura y eficiente. 222 | --------------------------------------------------------------------------------