├── .github └── workflows │ └── deploy-site.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── Cargo.toml ├── README.md ├── Trunk.toml ├── assets │ ├── images │ │ ├── $dark_$mobile_banner.png │ │ ├── $dark_$mobile_banner.svg │ │ ├── $dark_$mobile_projects_banner.svg │ │ ├── $dark_banner.png │ │ ├── $dark_banner.svg │ │ ├── $dark_close_btn.svg │ │ ├── $dark_drawer.svg │ │ ├── $dark_github.svg │ │ ├── $dark_projects_banner.svg │ │ ├── $dark_setting.svg │ │ ├── $dark_zzhack_logo.png │ │ ├── $dark_zzhack_logo.svg │ │ ├── $light_$mobile_banner.png │ │ ├── $light_$mobile_banner.svg │ │ ├── $light_$mobile_projects_banner.svg │ │ ├── $light_banner.png │ │ ├── $light_banner.svg │ │ ├── $light_close_btn.svg │ │ ├── $light_drawer.svg │ │ ├── $light_github.svg │ │ ├── $light_projects_banner.svg │ │ ├── $light_setting.svg │ │ ├── $light_zzhack_logo.png │ │ ├── $light_zzhack_logo.svg │ │ ├── $mobile_links_banner.svg │ │ ├── about_compare.svg │ │ ├── about_design_lang.svg │ │ ├── about_zzhack.svg │ │ ├── auto_mode_skeleton.png │ │ ├── avatar.png │ │ ├── dark_mode.svg │ │ ├── dark_mode_skeleton.png │ │ ├── discord.svg │ │ ├── gmail.svg │ │ ├── guide_post.png │ │ ├── label.png │ │ ├── light_mode.svg │ │ ├── light_mode_skeleton.png │ │ ├── links_banner.svg │ │ ├── page_not_found.png │ │ ├── switch_theme_guide.png │ │ ├── switch_theme_guide.svg │ │ ├── twitter.svg │ │ ├── wechat.svg │ │ └── zzhack_favicon.svg │ ├── sources │ │ ├── blog_cover.svg │ │ ├── dark_mode_skeleton.png │ │ ├── dynamic_dispatch.png │ │ ├── into_the_wild.jpg │ │ ├── issues_dispatch.png │ │ ├── links_cover.png │ │ ├── links_cover.svg │ │ ├── mlog_cover.png │ │ ├── provider_dispatch.png │ │ ├── static_dispatch.png │ │ ├── wasm_fib.png │ │ └── yew_logo.png │ └── styles │ │ ├── base.scss │ │ ├── markdown.scss │ │ └── one-dark.scss ├── configs │ ├── md_translate_rs.template │ ├── rs_aggregation.template │ └── rs_iteration.template ├── index.html └── src │ ├── app.rs │ ├── main.rs │ ├── posts.rs │ └── routes.rs ├── doc ├── zzhack_banner.png ├── zzhack_dark_mode.png ├── zzhack_light_mode.png ├── zzhack_links.png └── zzhack_sketch.png ├── global ├── .gitignore ├── Cargo.toml └── src │ ├── lib.rs │ └── theme_context.rs ├── pages ├── about │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── home │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── links │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── not_found │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── post │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── projects │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── lib.rs ├── posts ├── add_links.md ├── build_blog.md └── mlog_2022-10-26.md ├── router ├── Cargo.toml └── src │ └── lib.rs ├── services ├── .gitignore ├── Cargo.toml └── src │ ├── lib.rs │ ├── links_service │ ├── links.json │ ├── links_service.rs │ └── mod.rs │ ├── markdown_service │ ├── elements.rs │ ├── markdown_service.rs │ └── mod.rs │ ├── post_service │ ├── mod.rs │ ├── post_card_size.rs │ └── post_service.rs │ ├── posts.rs │ ├── projects_service │ ├── mod.rs │ ├── projects.json │ └── projects_service.rs │ └── theme_service │ ├── mod.rs │ ├── theme.rs │ └── theme_service.rs ├── templates ├── md_parser.template └── md_parser_iteration.template ├── ui ├── .gitignore ├── Cargo.toml └── src │ ├── common │ ├── contact.rs │ ├── container.rs │ ├── footer.rs │ ├── gradient_title.rs │ ├── header │ │ ├── drawer.rs │ │ ├── drawer_item.rs │ │ ├── header.rs │ │ └── mod.rs │ ├── image.rs │ ├── layout.rs │ ├── link.rs │ ├── mod.rs │ ├── modal │ │ ├── mod.rs │ │ ├── modal.rs │ │ ├── modal_action.rs │ │ └── modal_content.rs │ └── switch.rs │ ├── lib.rs │ ├── link_card.rs │ ├── post_card │ ├── label.rs │ ├── mod.rs │ ├── post_card.rs │ ├── post_card_header.rs │ ├── post_card_large.rs │ └── post_card_small.rs │ ├── project_card.rs │ └── theme │ ├── mod.rs │ ├── theme_item.rs │ └── theme_selector.rs ├── utils ├── .gitignore ├── Cargo.toml └── src │ ├── error.rs │ ├── html.rs │ ├── lib.rs │ ├── logger.rs │ ├── resource.rs │ ├── style.rs │ ├── theme.rs │ └── time.rs └── vercel.json /.github/workflows/deploy-site.yml: -------------------------------------------------------------------------------- 1 | name: deploy-site 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | cargo-build: 9 | name: vercel deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: hecrj/setup-rust-action@v1 15 | with: 16 | rust-version: stable 17 | 18 | - name: 'install wasm32-unknown-unknown' 19 | run: rustup target add wasm32-unknown-unknown 20 | 21 | - uses: jetli/wasm-bindgen-action@v0.1.0 22 | with: 23 | version: 'latest' 24 | 25 | - uses: actions-rs/install@v0.1 26 | with: 27 | crate: rapper 28 | version: latest 29 | 30 | - uses: jetli/trunk-action@v0.1.0 31 | with: 32 | version: 'latest' 33 | 34 | - name: build 35 | run: cd app && trunk build --release -d ../dist 36 | 37 | - uses: amondnet/vercel-action@v20 38 | with: 39 | vercel-cli: vercel 40 | vercel-token: ${{ secrets.VERCEL_TOKEN }} 41 | vercel-args: --prod 42 | vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} 43 | vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | .vercel 3 | 4 | # Dependencies 5 | pkg 6 | /target 7 | 8 | # Dist 9 | dist 10 | 11 | # Platform 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 4, 5 | "[rust]": { 6 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "app", 5 | "ui", 6 | "utils", 7 | "services", 8 | "global", 9 | "pages/home", 10 | "pages/projects", 11 | "pages/not_found", 12 | "pages/home", 13 | "pages/links", 14 | "pages/post", 15 | "router", 16 | ] 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![zzhack_banner](https://raw.githubusercontent.com/zzhack-stack/zzhack/main/doc/zzhack_banner.png) 2 |

zzhack

3 | 4 | zzhack is a simple but beauty WASM App template, which based on by Rust & Yew, if you want to create your own WASM site using zzhack template, just get start by [zzhack-cli](https://github.com/zzhack-stack/zzhack-cli) 5 | 6 | ![sketch](https://raw.githubusercontent.com/zzhack-stack/zzhack/main/doc/zzhack_sketch.png) 7 | 8 | ## License 9 | GNU GPLv3. 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zzhack" 3 | version = "0.1.0" 4 | authors = ["mistricky "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | yew = "0.19.3" 9 | wasm-bindgen = "0.2.80" 10 | material-yew = { git = "https://github.com/hamza1311/material-yew", features = ["full"] } 11 | stdweb = "0.4.20" 12 | css-in-rust = "0.5.0" 13 | web-sys = { version = "0.3", features = ["HtmlMetaElement", "Document", "Element", "DocumentFragment", "HtmlTemplateElement", "MediaQueryList"] } 14 | pulldown-cmark = "0.8.0" 15 | once_cell = "1.8.0" 16 | anyhow = "1.0.41" 17 | serde = "1.0.126" 18 | regex = "1.5.4" 19 | serde_json = "1.0.64" 20 | chrono = "0.4.19" 21 | yew-router = "0.16.0" 22 | base64 = "0.13.0" 23 | js-sys = "0.3.55" 24 | lazy_static = "1.4.0" 25 | stylist = {version = "0.10", features = ["yew_integration"]} 26 | wasm-logger = "0.2.0" 27 | log = "0.4.17" 28 | wee_alloc = "0.4.5" 29 | services = {path = "../services"} 30 | global = {path = "../global"} 31 | utils = {path = "../utils"} 32 | ui = {path = "../ui"} 33 | post = {path = "../pages/post"} 34 | home = {path = "../pages/home"} 35 | about = {path = "../pages/about"} 36 | not_found = {path = "../pages/not_found"} 37 | projects = {path = "../pages/projects"} 38 | links = {path = "../pages/links"} 39 | router = {path = "../router"} 40 | 41 | [profile.release] 42 | lto = true 43 | panic = 'abort' 44 | codegen-units = 1 45 | 46 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # zzhack 2 | My personal blog site which based on by Rust & Yew, the motivation for build this site is just I want a place where I can thinking and write it down. 3 | 4 | ## UI Design 5 | Not surprisingly, I designed three versions of the design, actually as you can see the current version is 3.0.0 version. 6 | 7 | ![sketch](https://raw.githubusercontent.com/zzhack-stack/zzhack/main/doc/zzhack_sketch.png) 8 | 9 | It's worth to mention that, oh I love dark mode, so the `zzhack` also support dark mode of course. 10 | 11 | ![zzhack dark mode](https://raw.githubusercontent.com/zzhack-stack/zzhack/main/doc/zzhack_dark_mode.png) 12 | 13 | ![zzhack light mode](https://raw.githubusercontent.com/zzhack-stack/zzhack/main/doc/zzhack_light_mode.png) 14 | 15 | If you have some advice for design, please contact me by [email](Mailto:mist.zzh@gmail.com), also if you want my sketches of zzhack, just send me email. 16 | 17 | ## Links 18 | The zzhack have a full page to display links, so if you wanna add your website to zzhack, please comment in [here](https://github.com/zzhack-stack/zzhack/issues/4). 19 | 20 | ![zzhack links](https://raw.githubusercontent.com/zzhack-stack/zzhack/main/doc/zzhack_links.png) 21 | 22 | 23 | ## License 24 | MIT. 25 | -------------------------------------------------------------------------------- /app/Trunk.toml: -------------------------------------------------------------------------------- 1 | # rapper reduce -t ./templates/md_parser.template -i ./templates/md_parser_iteration.template --target ./posts/ -e md -d ./services/src/posts.rs 2 | [[hooks]] 3 | stage = "pre_build" 4 | command = "rapper" 5 | command_arguments = ["reduce", "-t", "../templates/md_parser.template", "-i", "../templates/md_parser_iteration.template", "--target", "../posts/", "-e", "md", "-d", "../services/src/posts.rs"] 6 | -------------------------------------------------------------------------------- /app/assets/images/$dark_$mobile_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/$dark_$mobile_banner.png -------------------------------------------------------------------------------- /app/assets/images/$dark_$mobile_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Do more useless things 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/images/$dark_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/$dark_banner.png -------------------------------------------------------------------------------- /app/assets/images/$dark_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Do more useless things 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/images/$dark_close_btn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/$dark_drawer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/$dark_github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/github_dark 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/assets/images/$dark_projects_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | @mistricky 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/assets/images/$dark_setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | foo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/images/$dark_zzhack_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/$dark_zzhack_logo.png -------------------------------------------------------------------------------- /app/assets/images/$dark_zzhack_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | r 4 | 5 | 6 | 7 | zzhack 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/assets/images/$light_$mobile_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/$light_$mobile_banner.png -------------------------------------------------------------------------------- /app/assets/images/$light_$mobile_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Do more useless things 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/images/$light_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/$light_banner.png -------------------------------------------------------------------------------- /app/assets/images/$light_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Do more useless things 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/images/$light_close_btn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/$light_drawer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/assets/images/$light_github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/github_dark 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/assets/images/$light_projects_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | @mistricky 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/assets/images/$light_setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | foo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/images/$light_zzhack_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/$light_zzhack_logo.png -------------------------------------------------------------------------------- /app/assets/images/$light_zzhack_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | r 4 | 5 | 6 | 7 | zzhack 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/assets/images/about_zzhack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | 5 | 6 | 7 | 8 | 9 | Version 5 10 | 11 | 12 | zzhack 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/assets/images/auto_mode_skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/auto_mode_skeleton.png -------------------------------------------------------------------------------- /app/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/avatar.png -------------------------------------------------------------------------------- /app/assets/images/dark_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | dark_mode 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/dark_mode_skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/dark_mode_skeleton.png -------------------------------------------------------------------------------- /app/assets/images/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bitmap 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/gmail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bitmap 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/guide_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/guide_post.png -------------------------------------------------------------------------------- /app/assets/images/label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/label.png -------------------------------------------------------------------------------- /app/assets/images/light_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | light_mode 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/light_mode_skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/light_mode_skeleton.png -------------------------------------------------------------------------------- /app/assets/images/page_not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/page_not_found.png -------------------------------------------------------------------------------- /app/assets/images/switch_theme_guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/images/switch_theme_guide.png -------------------------------------------------------------------------------- /app/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bitmap 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/wechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bitmap 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/zzhack_favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | 5 | 6 | 7 | 8 | z 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/sources/dark_mode_skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/dark_mode_skeleton.png -------------------------------------------------------------------------------- /app/assets/sources/dynamic_dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/dynamic_dispatch.png -------------------------------------------------------------------------------- /app/assets/sources/into_the_wild.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/into_the_wild.jpg -------------------------------------------------------------------------------- /app/assets/sources/issues_dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/issues_dispatch.png -------------------------------------------------------------------------------- /app/assets/sources/links_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/links_cover.png -------------------------------------------------------------------------------- /app/assets/sources/mlog_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/mlog_cover.png -------------------------------------------------------------------------------- /app/assets/sources/provider_dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/provider_dispatch.png -------------------------------------------------------------------------------- /app/assets/sources/static_dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/static_dispatch.png -------------------------------------------------------------------------------- /app/assets/sources/wasm_fib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/wasm_fib.png -------------------------------------------------------------------------------- /app/assets/sources/yew_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/app/assets/sources/yew_logo.png -------------------------------------------------------------------------------- /app/assets/styles/base.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --mdc-theme-primary: #7ECCB4; 3 | 4 | mwc-icon-button::part("mdc-icon-button") { 5 | background: red; 6 | } 7 | 8 | .light { 9 | --primary-color: #7ECCB4; 10 | --base-color: #fff; 11 | --underlay-color: #F3F7FD; 12 | --text-color: #3F3D55; 13 | --card-color: #fff; 14 | --mask-color: rgba(0, 0, 0, 0.45); 15 | --shallow-gray: rgba(112, 112, 112, 0.06); 16 | --sub-text-color: rgba(63, 61, 85, 0.63); 17 | --primary-shadow-color: rgb(127 214 194 / 22%); 18 | --card-shadow-color: rgba(149, 157, 165, 0.2); 19 | --label-color: rgba(126, 204, 180, 0.36); 20 | --tip-color: rgba(63, 61, 85, 0.39); 21 | --code-block-bg: #282c34; 22 | --alert-color: rgba(32, 136, 255, 0.2); 23 | --blue: #4794FE; 24 | --code-color: var(--text-color); 25 | --code-bg: rgba(126, 204, 180, 0.35); 26 | } 27 | 28 | .dark { 29 | --code-bg: rgba(126, 204, 180, 0.35); 30 | --code-color: var(--text-color); 31 | --blue: #4794FE; 32 | --alert-color: rgba(32, 136, 255, 0.2); 33 | --code-block-bg: #282c34; 34 | --label-color: rgba(126, 204, 180, 0.36); 35 | --card-shadow-color: rgba(149, 157, 165, 0); 36 | --primary-color: #7ECCB4; 37 | --primary-shadow-color: rgb(127 214 194 / 22%); 38 | --base-color: #31363F; 39 | --underlay-color: #24292E; 40 | --text-color: #D6D8DA; 41 | --card-color: #31363F; 42 | --mask-color: rgba(0, 0, 0, 0.83); 43 | --shallow-gray: rgba(216, 216, 216, 0.08); 44 | --sub-text-color: #768390; 45 | --tip-color: #57606A; 46 | } 47 | } 48 | 49 | .text { 50 | color: var(--normal-text-color); 51 | } 52 | 53 | .article-text { 54 | color: var(--article-text-color); 55 | } 56 | 57 | .mini-container { 58 | width: 600px; 59 | margin: auto; 60 | } 61 | 62 | .article-container { 63 | width: 760px; 64 | margin: auto; 65 | } 66 | 67 | .non-style-link { 68 | text-decoration: none; 69 | } 70 | 71 | .articles { 72 | background: var(--base-color); 73 | border-radius: 3px; 74 | box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; 75 | width: 100%; 76 | padding: 10px 15px; 77 | } 78 | 79 | .card { 80 | border: 1px solid var(--border-color); 81 | transition: box-shadow 0.3s; 82 | box-shadow: rgba(9, 30, 66, 0.25) 0px 0px 0px; 83 | 84 | &:hover { 85 | box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px; 86 | border-color: transparent; 87 | } 88 | } 89 | 90 | @media (max-width: 600px) { 91 | .container, .mini-container { 92 | width: 100%; 93 | overflow: hidden; 94 | padding: 0 20px; 95 | box-sizing: border-box; 96 | } 97 | 98 | .article-container { 99 | width: 100%; 100 | overflow: hidden; 101 | padding: 10px 30px; 102 | box-sizing: border-box; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/assets/styles/markdown.scss: -------------------------------------------------------------------------------- 1 | .markdown-heading { 2 | font-size: 25px; 3 | transition: 0.1s all; 4 | cursor: pointer; 5 | position: relative; 6 | 7 | &:hover { 8 | color: var(--mdc-theme-primary); 9 | } 10 | } 11 | 12 | .markdown-heading-anchor { 13 | text-decoration: none; 14 | } 15 | 16 | .markdown-img-container { 17 | max-width: 660px; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | flex-direction: column; 22 | margin: 30px 0; 23 | 24 | .markdown-img { 25 | max-width: 660px; 26 | max-height: 400px; 27 | border-radius: 5px; 28 | } 29 | 30 | .markdown-img-alt { 31 | margin-top: 10px; 32 | } 33 | } 34 | 35 | blockquote { 36 | border-left: 8px solid var(--mdc-theme-primary); 37 | margin: 0; 38 | padding: 10px; 39 | background: rgba(57, 113, 245, 0.1); 40 | border-radius: 5px; 41 | padding-left: 15px; 42 | 43 | p { 44 | color: var(--blockquote-color) !important; 45 | font-size: 14px; 46 | margin: 0; 47 | } 48 | } 49 | 50 | .markdown-spotlight { 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | margin: 50px 0; 55 | 56 | .markdown-spotlight__content { 57 | text-align: center; 58 | font-style: italic; 59 | margin: 0 20px; 60 | font-size: 18px; 61 | } 62 | 63 | .markdown-spotlight__icon { 64 | width: 25px; 65 | } 66 | } 67 | 68 | .markdown-github-render-block-container { 69 | .markdown-github-render-block-link { 70 | text-decoration: none; 71 | } 72 | 73 | .markdown-github-render-block { 74 | max-width: 450px; 75 | width: fit-content; 76 | overflow: hidden; 77 | border-radius: 5px; 78 | background: var(--mdc-theme-primary); 79 | padding: 15px; 80 | box-sizing: border-box; 81 | display: flex; 82 | margin: 25px 0; 83 | align-items: center; 84 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 85 | 86 | .markdown-github-render-block-icon { 87 | width: 50px; 88 | } 89 | 90 | .markdown-github-render-block-info { 91 | margin-left: 15px; 92 | 93 | .markdown-github-render-block-repo { 94 | color: white; 95 | font-size: 16px; 96 | } 97 | 98 | .markdown-github-render-block-desc { 99 | color: white; 100 | font-size: 14px; 101 | line-height: 1.5; 102 | } 103 | } 104 | 105 | .markdown-github-render-block-goto { 106 | width: 35px; 107 | height: 35px; 108 | flex-shrink: 0; 109 | background: #fff; 110 | border-radius: 50%; 111 | display: flex; 112 | justify-content: center; 113 | align-items: center; 114 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 115 | } 116 | } 117 | } 118 | 119 | .markdown-code { 120 | background: var(--code-bg); 121 | padding: 3px 7px; 122 | border-radius: 5px; 123 | font-size: 14px; 124 | color: var(--code-color) !important; 125 | } 126 | 127 | .markdown-code-block { 128 | padding: 10px 15px; 129 | background: var(--code-block-bg); 130 | border-radius: 5px; 131 | box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 3px 0px, rgba(27, 31, 35, 0.15) 0px 0px 0px 1px; 132 | 133 | .markdown-mac-control-bars { 134 | display: flex; 135 | align-items: center; 136 | 137 | .control-bar { 138 | width: 13px; 139 | height: 13px; 140 | margin-right: 8px; 141 | background: #fff; 142 | border-radius: 50%; 143 | } 144 | 145 | .markdown-mac-close-bar { 146 | @extend .control-bar; 147 | 148 | background: #ff5f57; 149 | } 150 | .markdown-mac-min-bar { 151 | @extend .control-bar; 152 | 153 | background: #febc2e; 154 | } 155 | .markdown-mac-max-bar { 156 | @extend .control-bar; 157 | 158 | background: #28c840; 159 | } 160 | } 161 | 162 | pre { 163 | background-color: var(--code-block-bg) !important; 164 | white-space: break-spaces; 165 | } 166 | } 167 | 168 | .markdown-container { 169 | * { 170 | color: var(--article-text-color); 171 | } 172 | } 173 | 174 | @media (max-width: 600px){ 175 | .markdown-img-container { 176 | max-width: 100%; 177 | display: flex; 178 | justify-content: center; 179 | align-items: center; 180 | flex-direction: column; 181 | 182 | .markdown-img { 183 | max-width: 100%; 184 | border-radius: 5px; 185 | } 186 | } 187 | 188 | .markdown-spotlight { 189 | .markdown-spotlight__icon { 190 | width: 18px; 191 | } 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /app/assets/styles/one-dark.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Atom One Dark by Daniel Gamage 3 | Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax 4 | base: #282c34 5 | mono-1: #abb2bf 6 | mono-2: #818896 7 | mono-3: #5c6370 8 | hue-1: #56b6c2 9 | hue-2: #61aeee 10 | hue-3: #c678dd 11 | hue-4: #98c379 12 | hue-5: #e06c75 13 | hue-5-2: #be5046 14 | hue-6: #d19a66 15 | hue-6-2: #e6c07b 16 | */ 17 | 18 | .hljs { 19 | color: #abb2bf; 20 | background: #282c34; 21 | } 22 | 23 | .hljs-comment, 24 | .hljs-quote { 25 | color: #5c6370; 26 | font-style: italic; 27 | } 28 | 29 | .hljs-doctag, 30 | .hljs-keyword, 31 | .hljs-formula { 32 | color: #c678dd; 33 | } 34 | 35 | .hljs-section, 36 | .hljs-name, 37 | .hljs-selector-tag, 38 | .hljs-deletion, 39 | .hljs-subst { 40 | color: #e06c75; 41 | } 42 | 43 | .hljs-literal { 44 | color: #56b6c2; 45 | } 46 | 47 | .hljs-string, 48 | .hljs-regexp, 49 | .hljs-addition, 50 | .hljs-attribute, 51 | .hljs-meta .hljs-string { 52 | color: #98c379; 53 | } 54 | 55 | .hljs-attr, 56 | .hljs-variable, 57 | .hljs-template-variable, 58 | .hljs-type, 59 | .hljs-selector-class, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo, 62 | .hljs-number { 63 | color: #d19a66; 64 | } 65 | 66 | .hljs-symbol, 67 | .hljs-bullet, 68 | .hljs-link, 69 | .hljs-meta, 70 | .hljs-selector-id, 71 | .hljs-title { 72 | color: #61aeee; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-title.class_, 77 | .hljs-class .hljs-title { 78 | color: #e6c07b; 79 | } 80 | 81 | .hljs-emphasis { 82 | font-style: italic; 83 | } 84 | 85 | .hljs-strong { 86 | font-weight: bold; 87 | } 88 | 89 | .hljs-link { 90 | text-decoration: underline; 91 | } -------------------------------------------------------------------------------- /app/configs/md_translate_rs.template: -------------------------------------------------------------------------------- 1 | static const content: &'static str = r#"{{TEMPLATE}}"#; 2 | -------------------------------------------------------------------------------- /app/configs/rs_aggregation.template: -------------------------------------------------------------------------------- 1 | pub static posts: [&'static str; {{TRAVERSE_COUNT}}] = [ 2 | {{TEMPLATE}} 3 | ]; 4 | -------------------------------------------------------------------------------- /app/configs/rs_iteration.template: -------------------------------------------------------------------------------- 1 | r#"{{TEMPLATE}}"#, 2 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | zzhack 32 | 33 | 34 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::routes::RouteOutlet; 2 | use global::theme_context::ThemeProvider; 3 | use yew::prelude::*; 4 | 5 | #[function_component(App)] 6 | pub fn app() -> Html { 7 | html! { 8 | 9 | 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod routes; 3 | 4 | extern crate lazy_static; 5 | extern crate wee_alloc; 6 | 7 | use app::App; 8 | use yew::start_app; 9 | 10 | // Use `wee_alloc` as the global allocator. 11 | #[global_allocator] 12 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 13 | 14 | fn main() { 15 | wasm_logger::init(wasm_logger::Config::default()); 16 | 17 | start_app::(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/posts.rs: -------------------------------------------------------------------------------- 1 | pub static posts: [&'static str; 9] = [ 2 | r#"# A 3 | abc 4 | "#, 5 | r#"# A 6 | abc 7 | "#, 8 | r#"# A 9 | abc 10 | "#, 11 | r#"# A 12 | abc 13 | "#, 14 | r#"# A 15 | abc 16 | "#, 17 | r#"# A 18 | abc 19 | "#, 20 | r#"# A 21 | abc 22 | "#, 23 | r#"# A 24 | abc 25 | "#, 26 | r#"# A 27 | abc 28 | "#, 29 | 30 | ]; 31 | -------------------------------------------------------------------------------- /app/src/routes.rs: -------------------------------------------------------------------------------- 1 | use post::Post; 2 | use ui::layout::BaseLayout; 3 | use yew::prelude::*; 4 | use yew_router::prelude::*; 5 | 6 | use about::About; 7 | use home::Home; 8 | use links::Links; 9 | use not_found::NotFound; 10 | use projects::Projects; 11 | use router::RootRoutes; 12 | 13 | fn switch(routes: &RootRoutes) -> Html { 14 | match routes { 15 | RootRoutes::Home | RootRoutes::Root => html! { }, 16 | RootRoutes::Projects => html! { }, 17 | RootRoutes::About => html! { }, 18 | RootRoutes::Post { filename } => html! {}, 19 | RootRoutes::NotFound => html! { }, 20 | RootRoutes::Technology => html! { 21 | to={RootRoutes::Home}/> 22 | }, 23 | RootRoutes::Links => html! {}, 24 | } 25 | } 26 | 27 | #[function_component(RouteOutlet)] 28 | pub fn route_outlet() -> Html { 29 | html! { 30 | 31 | 32 | render={Switch::render(switch)} /> 33 | 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /doc/zzhack_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/doc/zzhack_banner.png -------------------------------------------------------------------------------- /doc/zzhack_dark_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/doc/zzhack_dark_mode.png -------------------------------------------------------------------------------- /doc/zzhack_light_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/doc/zzhack_light_mode.png -------------------------------------------------------------------------------- /doc/zzhack_links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/doc/zzhack_links.png -------------------------------------------------------------------------------- /doc/zzhack_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhack-stack/zzhack/a7adb33e2348b584fb56e5def39ddc0dbad680c6/doc/zzhack_sketch.png -------------------------------------------------------------------------------- /global/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /global/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "global" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | services = {path = "../services"} 11 | wasm-logger = "0.2.0" 12 | log = "0.4.17" 13 | -------------------------------------------------------------------------------- /global/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod theme_context; 2 | -------------------------------------------------------------------------------- /global/src/theme_context.rs: -------------------------------------------------------------------------------- 1 | use services::theme_service::Theme; 2 | use services::theme_service::ThemeService; 3 | use std::rc::Rc; 4 | use yew::prelude::*; 5 | 6 | pub enum ThemeAction { 7 | UpdateTheme(Theme), 8 | } 9 | 10 | #[derive(Debug, PartialEq, Clone)] 11 | pub struct ThemeState { 12 | pub theme: Theme, 13 | } 14 | 15 | impl Reducible for ThemeState { 16 | type Action = ThemeAction; 17 | 18 | fn reduce(self: Rc, action: Self::Action) -> Rc { 19 | match action { 20 | ThemeAction::UpdateTheme(theme) => { 21 | ThemeService::from_storage().set_theme(&theme); 22 | 23 | // If the theme is auto, convert auto to actually theme before dispatch theme in components tree 24 | Rc::from(ThemeState { 25 | theme: ThemeService::convert_auto_to_actually_theme(theme), 26 | }) 27 | } 28 | } 29 | } 30 | } 31 | 32 | pub type ThemeContext = UseReducerHandle; 33 | 34 | #[derive(Properties, Debug, PartialEq)] 35 | pub struct ThemeProviderProps { 36 | pub children: Children, 37 | } 38 | 39 | #[function_component(ThemeProvider)] 40 | pub fn theme_provider(props: &ThemeProviderProps) -> Html { 41 | let theme = use_reducer_eq(|| { 42 | let theme = ThemeService::from_storage().get_theme().clone(); 43 | 44 | ThemeState { 45 | theme: ThemeService::convert_auto_to_actually_theme(theme), 46 | } 47 | }); 48 | 49 | html! { 50 | context={theme}> 51 | { props.children.clone() } 52 | > 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pages/about/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /pages/about/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "about" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | ui = {path = "../../ui"} 11 | utils = {path = "../../utils"} 12 | stylist = {version = "0.10", features = ["yew_integration"]} 13 | services = {path = "../../services"} 14 | 15 | -------------------------------------------------------------------------------- /pages/about/src/lib.rs: -------------------------------------------------------------------------------- 1 | use stylist::style; 2 | use ui::contact::ContactType; 3 | use ui::gradient_title::GradientTitle; 4 | use ui::link::Link; 5 | use yew::prelude::*; 6 | 7 | #[function_component(About)] 8 | pub fn about() -> Html { 9 | let style = style!( 10 | r" 11 | padding: 50px 0; 12 | 13 | a { 14 | color: var(--blue); 15 | font-size: 16px; 16 | } 17 | 18 | .illustrate { 19 | margin: 20px 0; 20 | border-radius: 20px; 21 | } 22 | 23 | @media (max-width: 600px) { 24 | .illustrate { 25 | width: 100%; 26 | } 27 | } 28 | " 29 | ) 30 | .unwrap(); 31 | 32 | html! { 33 |
34 |
35 | {"zzhack 的诞生"} 36 |
37 |

38 | {"嗨!欢迎来到我的应用 zzhack 😎,这是一个兴趣使然的项目,zzhack 被设计为一个注重信息展示的应用,它是序列化和沉淀我思想的地方。"} 39 |

40 |

41 | {"如你所见的 zzhack 已是第五个大版本,它已经经过了两次大规模的重构以及 5 次重新设计,最后回归纯真,专注信息展示。"} 42 |

43 |
44 | 45 |

46 | {"这么看下来 zzhack 的确没有什么让人惊讶的亮点,没有额外的用户交互,没有炫酷的交互动画,看上去只是一个平静的展示内容的 web 应用,但是它的确适合作为一个单纯的内容输出的站点,而不被逐渐社交化。"} 47 |

48 |

49 | {"zzhack 是一个纯静态的应用,并且开源内容到代码的所有,如果你对它的技术实现感兴趣可以在 "} 50 | {"这里"} 51 | {" 找到它。"} 52 |

53 |
54 |
55 | {"关于我"} 56 |

57 | {"我叫 Mist,一名全栈工程师。"} 58 |

59 |
60 |
61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pages/home/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /pages/home/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "home" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | ui = {path = "../../ui"} 11 | utils = {path = "../../utils"} 12 | stylist = {version = "0.10", features = ["yew_integration"]} 13 | services = {path = "../../services"} 14 | -------------------------------------------------------------------------------- /pages/home/src/lib.rs: -------------------------------------------------------------------------------- 1 | use services::post_service::post_service::{FilterTag, POST_SERVICE}; 2 | use ui::image::ThemeImage; 3 | use ui::label::Label; 4 | use ui::link::Link; 5 | use ui::post_card::post_card::PostCard; 6 | use utils::use_style; 7 | use yew::prelude::*; 8 | 9 | #[function_component(Home)] 10 | pub fn home() -> Html { 11 | let style = use_style!( 12 | r" 13 | .banner { 14 | width: 100%; 15 | display: flex; 16 | justify-content: center; 17 | margin-top: 40px; 18 | } 19 | 20 | .banner > img { 21 | width: 100%; 22 | } 23 | 24 | .labels { 25 | width: 100%; 26 | display: flex; 27 | flex-wrap: wrap; 28 | margin-top: 33px; 29 | } 30 | 31 | .label { 32 | margin-left: 18px; 33 | } 34 | 35 | .posts { 36 | margin-bottom: 45px; 37 | margin-top: 39px; 38 | display: flex; 39 | flex-wrap: wrap; 40 | margin: 30px -18px 45px -18px; 41 | } 42 | 43 | @media (max-width: 600px) { 44 | .banner { 45 | width: 100%; 46 | margin-top: 32px; 47 | } 48 | 49 | .posts { 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | margin: 39px 0 45px 0; 54 | } 55 | } 56 | " 57 | ); 58 | let posts = use_state_eq(|| POST_SERVICE.get_posts()); 59 | let handle_filter_posts_by_label = { 60 | let posts = posts.clone(); 61 | 62 | |tag: FilterTag| { 63 | Callback::from(move |_| { 64 | posts.set(POST_SERVICE.filter_post_by_tag(tag.clone())); 65 | }) 66 | } 67 | }; 68 | let handle_filter_posts_by_rest_label = handle_filter_posts_by_label.clone(); 69 | 70 | html! { 71 |
72 | 75 |
76 | 77 |
89 |
90 | { 91 | posts.iter().map(|post| { 92 | html! { 93 | 94 | } 95 | }).collect::() 96 | } 97 |
98 |
99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pages/links/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "links" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | ui = {path = "../../ui"} 11 | utils = {path = "../../utils"} 12 | stylist = {version = "0.10", features = ["yew_integration"]} 13 | services = {path = "../../services"} 14 | router = {path = "../../router"} 15 | -------------------------------------------------------------------------------- /pages/links/src/lib.rs: -------------------------------------------------------------------------------- 1 | use router::RootRoutes; 2 | use services::links_service::links_service::LINKS_SERVICE; 3 | use stylist::style; 4 | use ui::image::BaseImage; 5 | use ui::link::Link; 6 | use ui::link_card::LinkCard; 7 | use yew::prelude::*; 8 | 9 | #[function_component(Links)] 10 | pub fn links() -> Html { 11 | let style = style!( 12 | r" 13 | .banner { 14 | position: relative; 15 | margin-top: 63px; 16 | } 17 | 18 | .banner > img { 19 | width: 100%; 20 | height: 165.59px; 21 | } 22 | 23 | .banner__links { 24 | position: absolute; 25 | top: 45px; 26 | left: 35px; 27 | } 28 | 29 | .banner__links-title { 30 | color: #fff; 31 | font-size: 18px; 32 | } 33 | 34 | .banner__links-desc { 35 | color: rgba(255, 255, 255, 0.81); 36 | font-size: 14px; 37 | margin-top: 10px; 38 | width: 500px; 39 | line-height: 12px; 40 | } 41 | 42 | .banner__links-desc > a { 43 | color: var(--blue); 44 | font-size: 14px; 45 | } 46 | 47 | .links { 48 | margin: 0 -15px; 49 | display: flex; 50 | flex-wrap: wrap; 51 | margin-top: 50px; 52 | } 53 | 54 | @media (max-width: 600px) { 55 | .banner__links { 56 | top: auto; 57 | left: auto; 58 | bottom: 30px; 59 | width: 100%; 60 | word-break: break-all; 61 | padding: 0 20px; 62 | box-sizing: border-box; 63 | } 64 | 65 | .banner__links-desc { 66 | width: 100%; 67 | line-height: 20px; 68 | } 69 | 70 | .banner > img { 71 | height: 323.69px; 72 | } 73 | 74 | .links { 75 | width: 100%; 76 | margin: auto; 77 | margin-top: 20px; 78 | margin-bottom: 100px; 79 | } 80 | 81 | .links a { 82 | width: 100%; 83 | } 84 | } 85 | " 86 | ) 87 | .unwrap(); 88 | 89 | html! { 90 |
91 | 100 | 109 |
110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pages/not_found/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /pages/not_found/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "not_found" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | stylist = {version = "0.10", features = ["yew_integration"]} 11 | -------------------------------------------------------------------------------- /pages/not_found/src/lib.rs: -------------------------------------------------------------------------------- 1 | use stylist::style; 2 | use yew::prelude::*; 3 | 4 | #[function_component(NotFound)] 5 | pub fn not_found() -> Html { 6 | let style = style!( 7 | r" 8 | padding-top: 100px; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | 13 | & > img { 14 | width: 400px; 15 | } 16 | 17 | @media (max-width: 600px) { 18 | padding-top: 50px; 19 | 20 | & > img { 21 | width: 100%; 22 | } 23 | } 24 | " 25 | ) 26 | .unwrap(); 27 | 28 | html! { 29 |
30 | 31 |
32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pages/post/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "post" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | stylist = {version = "0.10", features = ["yew_integration"]} 11 | ui = {path = "../../ui"} 12 | services = {path = "../../services"} 13 | utils = {path = "../../utils"} 14 | web-sys = { version = "0.3", features = ["HtmlMetaElement", "Document", "Element", "DocumentFragment", "HtmlTemplateElement", "MediaQueryList"] } 15 | gloo-timers = "0.2.4" 16 | -------------------------------------------------------------------------------- /pages/post/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gloo_timers::callback::Timeout; 2 | use services::markdown_service::markdown_service::MarkdownService; 3 | use services::post_service::post_service::POST_SERVICE; 4 | use stylist::style; 5 | use ui::post_card_header::PostCardHeader; 6 | use yew::prelude::*; 7 | 8 | #[derive(Properties, Clone, PartialEq)] 9 | pub struct PostProps { 10 | pub filename: String, 11 | } 12 | 13 | #[function_component(Post)] 14 | pub fn post(props: &PostProps) -> Html { 15 | let post = POST_SERVICE.find_post_by_filename(&props.filename).unwrap(); 16 | let style = style!( 17 | r#" 18 | width: 660px; 19 | height: 100%; 20 | padding: 63px 0; 21 | margin: auto; 22 | 23 | .post-header { 24 | width: 180px; 25 | } 26 | 27 | .modified-at { 28 | color: var(--tip-color); 29 | margin-top: 13px; 30 | font-size: 13px; 31 | } 32 | 33 | .title { 34 | font-size: 25px; 35 | color: var(--text-color); 36 | } 37 | 38 | .cover { 39 | width: 660px; 40 | height: 258px; 41 | background-image: url("${cover}"); 42 | background-repeat: no-repeat; 43 | background-size: cover; 44 | background-position: 50% 50%; 45 | border-radius: 5px; 46 | margin: 30px 0; 47 | transition: all 0.2s ease-in; 48 | } 49 | 50 | @media (max-width: 600px) { 51 | width: 100%; 52 | 53 | .cover { 54 | width: 100%; 55 | height: 180px; 56 | } 57 | } 58 | "#, 59 | cover = post.metadata.cover.clone() 60 | ) 61 | .unwrap(); 62 | let post_body = MarkdownService::new(post.raw_content.clone().to_string()); 63 | let post_body = post_body.parse_to_element("base16-ocean.dark"); 64 | 65 | use_effect(move || { 66 | let timeout = Timeout::new(500, move || { 67 | position_heading_by_anchor(); 68 | }); 69 | 70 | || { 71 | timeout.forget(); 72 | } 73 | }); 74 | 75 | html! { 76 |
77 |
78 | 79 |
80 |
{&post.modified_time}
81 |
82 |

83 | {&post.metadata.title} 84 |

85 |
86 | {Html::VRef(post_body.clone().into())} 87 |
88 |
89 | } 90 | } 91 | 92 | fn position_heading_by_anchor() { 93 | let window = web_sys::window().unwrap(); 94 | let document = window.document().unwrap(); 95 | let location = document.location().unwrap(); 96 | let hash = location.hash().unwrap(); 97 | 98 | if hash != "" { 99 | return; 100 | } 101 | 102 | let heading_ele = document.get_element_by_id(&hash[1..]).unwrap(); 103 | 104 | heading_ele.scroll_into_view(); 105 | } 106 | -------------------------------------------------------------------------------- /pages/projects/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /pages/projects/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "projects" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | stylist = {version = "0.10", features = ["yew_integration"]} 11 | ui = {path = "../../ui"} 12 | services = {path = "../../services"} 13 | utils = {path = "../../utils"} 14 | -------------------------------------------------------------------------------- /pages/projects/src/lib.rs: -------------------------------------------------------------------------------- 1 | use services::projects_service::projects_service::Project; 2 | use services::projects_service::projects_service::PROJECTS_SERVICE; 3 | use stylist::style; 4 | use ui::contact::ContactType; 5 | use ui::image::ThemeImage; 6 | use ui::link::Link; 7 | use ui::ProjectCard; 8 | use utils::theme::is_on_mobile; 9 | use yew::prelude::*; 10 | 11 | #[function_component(Projects)] 12 | pub fn projects() -> Html { 13 | let style = style!( 14 | r" 15 | margin-top: 30px; 16 | 17 | & > a > img { 18 | width: 100%; 19 | } 20 | 21 | .alert{ 22 | width: 100%; 23 | background-color: var(--alert-color); 24 | padding: 14px 19px; 25 | box-sizing: border-box; 26 | border-radius: 10px; 27 | margin: 15px 0; 28 | } 29 | 30 | .alert > p { 31 | color: var(--text-color); 32 | line-height: 20px; 33 | font-size: 13px; 34 | } 35 | 36 | .alert > p > a { 37 | line-height: 20px; 38 | font-size: 13px; 39 | color: var(--blue); 40 | } 41 | 42 | .cards { 43 | display: flex; 44 | justify-content: space-between; 45 | margin-top: 40px; 46 | margin-bottom: 45px; 47 | } 48 | 49 | @media (max-width: 600px) { 50 | .cards { 51 | flex-direction: column; 52 | } 53 | } 54 | " 55 | ) 56 | .unwrap(); 57 | let render_project_card = |projects: Vec| -> Html { 58 | projects 59 | .into_iter() 60 | .map(|project| { 61 | html! { 62 | 63 | } 64 | }) 65 | .collect::() 66 | }; 67 | let render_waterfall_flow = || -> Html { 68 | let (odd, even) = PROJECTS_SERVICE.get_projects_by_odd_even(); 69 | 70 | html! { 71 | <> 72 |
73 | {render_project_card(odd)} 74 |
75 |
76 | {render_project_card(even)} 77 |
78 | 79 | } 80 | }; 81 | let render_linear_flow = || -> Html { 82 | let projects = PROJECTS_SERVICE.get_projects(); 83 | 84 | render_project_card(projects) 85 | }; 86 | 87 | let render_target_vnode = if is_on_mobile() { 88 | render_linear_flow() 89 | } else { 90 | render_waterfall_flow() 91 | }; 92 | 93 | html! { 94 |
95 | 96 | 97 | 98 |
99 |

{"我会用业余时间维护一些开源项目,包括不限于奇思妙想的产品,提升开发者体验的工具,库,框架。我目前在思考于 UI Design 和想要用 ❤️ 做好一个产品。"}

100 |

101 | {"如果你有任何相关的建议或者有趣问题的讨论,欢迎直接通过 "} 102 | {"邮件"} 103 | {" 联系我。"} 104 |

105 |
106 |
107 | {render_target_vnode} 108 |
109 |
110 | } 111 | } 112 | -------------------------------------------------------------------------------- /posts/add_links.md: -------------------------------------------------------------------------------- 1 | ```metadata 2 | { 3 | "cover": "../app/assets/sources/links_cover.svg", 4 | "tag": "Help", 5 | "title": "如何申请友情链接" 6 | } 7 | ``` 8 | 9 | Hii,欢迎来到我的站点,我很乐意跟你连接,你可以向我发起一个 PR 来进行友情链接的添加,你的网站将会被陈列在 [这里](/links)。 10 | 11 | 跟随以下几个步骤来将你的网站添加到 [友情链接](/links) 吧! 12 | 13 | #### 1. Fork & Update links 14 | 15 | Fork [zzhack](https://github.com/zzhack-stack/zzhack) 到你的 GitHub,修改 `/services/links_service/links.json` 添加你的网站信息。 16 | 17 | ```json 18 | { 19 | "links": [ 20 | { 21 | "name": "Busyops博客", 22 | "addr": "https://busyops.com/", 23 | "desc": "Hello Moon", 24 | "logo": "https://busyops.com/images/avatar.jpg" 25 | }, 26 | { 27 | "name": "Clay 的技术博客", 28 | "addr": "https://www.techgrow.cn", 29 | "desc": "用进废退 | 艺不压身", 30 | "logo": "https://www.techgrow.cn/img/head.jpg" 31 | }, 32 | { 33 | "name": "Christine的博客", 34 | "desc": "虽然我不够优秀,但我从未放弃过努力。", 35 | "logo": "https://christine-only.github.io/blog/logo.png", 36 | "addr": "https://christine-only.github.io/blog/" 37 | }, 38 | { 39 | "name": "Forever丿顾北博客", 40 | "addr": "https://forevergubei.gitee.io/myblod/", 41 | "desc": "一个追寻大佬脚步的小白", 42 | "logo": "https://forevergubei.gitee.io/myblod/logo.png" 43 | } 44 | // { 45 | // "name": "站点名称", 46 | // "addr": "站点链接", 47 | // "desc": "站点描述", 48 | // "logo": "站点 logo" 49 | // }, 50 | // <- 加到这里 :D 51 | ] 52 | } 53 | ``` 54 | 55 | #### 2. 提交修改 56 | 57 | 通过 GitHub 创建一个 PR 合并到 `main`,等待 merge 后就能在 [友情链接](/links) 看到你的站点了。 58 | 59 | #### Final 60 | 61 | 如果你嫌上述步骤太麻烦也不要紧,将你的网站信息通过 [Email](mailto:mist.zzh@gmail.com) 发给我,我会在空闲的时候处理 :D。 62 | -------------------------------------------------------------------------------- /posts/build_blog.md: -------------------------------------------------------------------------------- 1 | ```metadata 2 | { 3 | "cover": "../app/assets/sources/blog_cover.svg", 4 | "tag": "Genesis", 5 | "title": "构建静态纯粹的博客站点", 6 | "pined": true, 7 | "size": "large" 8 | } 9 | ``` 10 | 11 | Hello World! 12 | 13 | 重构了好久的 zzhack 终于又又又上线了,这么久的从 UI 到整个架构的重构属实太长了,上线的第一篇创世文章想写清楚 zzhack 的构建和设计思路,有关该站点的建设动机可以移步到 [这里](/about)。 14 | 15 | 正如你看到的 zzhack 是一个纯正的 wasm 应用,它由 [Rust](https://www.rust-lang.org/) & [Yew](https://yew.rs/) 来作为技术栈进行搭建。 16 | 17 | [Rust](https://www.rust-lang.org/) 一个看似跟前端半毛钱不搭边的语言,为什么会渗透到前端的技术栈来,一切都要得益于 [Web Assembly](https://webassembly.org/)(后称 wasm),那什么是 wasm?在 wasm 官网上有这么一段简要的概括: 18 | 19 | > WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications. 20 | 21 | 由此可见,wasm 被设计的初衷就是一个编译目标语言(Wasm is designed as a portable compilation target for programming languages),在此之前,很多语言也有尝试过把 JS 本身就当作一个编译目标,让自家的语言也能渗透进前端的技术栈,做到 full-stack language,比如 [Dart](https://dart.dev/) 的 [dart2js](https://dart.dev/tools/dart2js) 编译器,但 dart 的 webtool 做的并不是很理想,其根本原因在于从开发体验到执行性能来看并没有本质上的提升,这就导致大部分开发者还是倾向于用 JS 来进行开发,不仅性能跟 dart2js 编译出的 bundle 相差无几,在开发效率上得益于动态语言的天然优势,也把静态语言远远甩在身后。但 Dart 自家的 Angular 也天然支持 Dart 进行开发,那是另一回事了(作者觉得体验并不如用 TS 来搭配 Angular)。 22 | 23 | Wasm 出现的一个根本原因在于前端正在从 “脚本化” 踏入 “工程化”,日益复杂的前端项目不仅需要大部分的维护成本,在一些领域现有的性能已经满足不了,出现了动态语言的瓶颈。动态语言让编译过程无法去猜测变量的类型而去进行热优化,每一个类型的动态变更都会导致编译过程需要做一次去优化。当然期间也出现了诸如 [asm.js](http://asmjs.org/) 这样的编译目标,这里就不过多的进行介绍了。 24 | 25 | Wasm 让在浏览器里能跑的语言多了一种新的可能性并且带来了极高的性能,在一些需要高强度计算的场景可谓是及时雨,下图为fib 递归运算 js 和 wasm 的运行时间对比。 26 | 27 | ``` 28 | fib(n) = fib(n - 1) + fib(n - 2) 29 | ``` 30 | 31 | ![Wasm fibonacci](../app/assets/sources/wasm_fib.png) 32 | 33 | 单个 fib 的例子可以看出计算性能几乎高了一倍。 34 | 35 | 当然这也给前端技术栈带来了更多的可能性,现在的浏览器不光能跑 JS 也能跑 wasm,这样只要有语言能提供编译到 wasm 目标的编译器,那这个语言就可以渗透进前端技术栈了!是不是光听着就兴奋无比!但事实却不尽人意,虽然 wasm 提供了比 js 更强的运算能力,但它的设计初衷并不是为了替代 js,而是作为一种新型的突破浏览器应用运算瓶颈的解决方案,这样一来,wasm 只能负责与计算有关的操作,换言之 wasm 并不能操作 dom,因此我们需要一个更聪明的打包工具链,将 dom 操作和计算运算逻辑分散开来,将有关 dom 的操作全部交给 js,执行运算的逻辑都交付给 wasm,再由 js 来负责调用 wasm 模块。 36 | 37 | 与此同时,现代前端开发体验除了良好的编码体验外,本地服务器 + 热更新是并不可少的,能提供这样接近于 js 的工具链的语言的生态少之又少,目前 Rust 的生态是最成熟的,出现了诸如 [Trunk](https://github.com/thedodd/trunk) 这类优秀的打包工具(该站点就是基于 [Trunk](https://github.com/thedodd/trunk) 打包的)。 38 | 39 | ## ViewModel? 40 | 介绍完了工具链,但还需要一个模板。 41 | 42 | 现代前端开发大多是 SPA 居多,后续的 SSR,SSG 等概念其实已经跟传统(直接利用模板引擎在服务端全部渲染完毕)的 SSR 等概念大不相同了。在交互居多的 web2.0 时代,前端要解决的首要难题是模型到视图的映射关系,希望模型的更新能尽快的更新到视图,完成一次视图的响应。现在很多所谓数据驱动的框架都帮我们做了这些事情,那在 Wasm 生态中,有没有诸如此类的框架呢?答案是有的!这就是 [Yew](https://yew.rs/)。 43 | 44 | ![Yew](../app/assets/sources/yew_logo.png) 45 | 46 | [Yew](https://yew.rs/) 是 Rust wasm 生态中的一环,利用 rust 强大的 macro,提供类似 jsx 的模板来表示 UI,提供媲美 React 的 jsx 的模板体验,虽然现在是 `0.19` 版本离稳定还有一段距离,但整个社区强大活力让我感到这个框架强大的生命力。 47 | 48 | 在 Yew 0.19 版本中支持了 function component,对比 0.18 的 component 写法更加简洁,一个 yew fc 大概是这样的: 49 | 50 | [ui/gradient_title.rs](https://github.com/zzhack-stack/zzhack) 51 | 52 | ```rust 53 | use stylist::style; 54 | use yew::prelude::*; 55 | 56 | #[derive(Properties, Clone, PartialEq)] 57 | pub struct GradientTitleProps { 58 | pub children: Children, 59 | } 60 | 61 | #[function_component(GradientTitle)] 62 | pub fn gradient_title(props: &GradientTitleProps) -> Html { 63 | let style = style!( 64 | r" 65 | display: flex; 66 | margin-bottom: 21px; 67 | 68 | .gradient-title__content { 69 | font-size: 29px; 70 | position: relative; 71 | } 72 | 73 | .gradient-title__content::before { 74 | content: ''; 75 | width: 120%; 76 | display: block; 77 | height: 21px; 78 | border-radius: 100px; 79 | background: linear-gradient(90deg, #FF4AA8 0%, #F95D66 22%, #FE9C76 100%); 80 | position: absolute; 81 | z-index: -1; 82 | bottom: 0px; 83 | left: -5%; 84 | } 85 | " 86 | ) 87 | .unwrap(); 88 | 89 | html! { 90 |
91 |
92 | { props.children.clone() } 93 |
94 |
95 | } 96 | } 97 | ``` 98 | 99 | 需要注意的是这并不是另一个 React 的轮子,它更像一个具有 jsx 模板的 Rust 框架而已。 100 | 101 | ## 说说博客吧! 102 | 103 | 聊了那么多技术栈的东西,接下来转身来聊聊博客。 104 | 105 | 想要比较快速的搭建一个 blog,可能会想到 hexo 之类的工具,这些不在本文讨论的范畴内。但如果你想更高强度的 DIY 来搭建你的 blog site,可以接着看下去。 106 | 107 | 博客首先是一个内容展示平台,意味着应该会有不断更新的数据源,而搭建一个博客站点最首要的工作也应该是思考如何处理发布这些数据源的问题,是动态发布,还是静态发布。 108 | 109 | ### 动态发布 110 | 之前 zzhack V1.0 的数据源就是动态发布,这样做的好处是你可以将数据源和站点天然的隔离开来,不仅拥有更新数据源不用重新编译部署的能力,甚至在做跨端(这里指有多个客户端,比如 Web App,Native App 小程序等)的时候也能很方便的拿到数据源。 111 | 112 | ![动态发布](../app/assets/sources/dynamic_dispatch.png) 113 | 114 | 在 zzhack v1.0 中,文章数据是由 GitHub issue 进行管理发布的,Issues 是一个天然的 Markdown 编辑器,并且 Issues 的 Labels 也能很好的帮助管理文章,进行分类归档。同时,GitHub API 提供了一套完整的关于 Issue 的 restful API 这样一来,客户端只需要通过 GitHub API 来获取对应的文章数据源进行渲染即可。 115 | 116 | ![Issues 管理文章](../app/assets/sources/issues_dispatch.png) 117 | 118 | 这样看起来是很完美,发布渲染一气呵成,但鉴于国内的网络情况,访问 GitHub 实在太慢,经常超时,所以如果大陆的用户来访问 zzhack v1 会经常卡在一个 loading 状态(新文章)... 119 | 120 | 在 v3 的时候将这部分进行了改进,弃掉 GitHub Issues 来管理文章转而直接用一个 Git 仓库来管理,当 git 版本变更时触发 CI hook,将文章和文章里包含的静态资源统统上传到国内的 CDN,然后客户端再直接通过 CDN 获取文章数据,这样一来速度就会快很多了。 121 | 122 | 为此我专门用 py 写了个小 [CLI](https://github.com/zzhack-stack/zzhack-provider) 来进行发布动作(Archive)。 123 | 124 | ![脚本发布](../app/assets/sources/provider_dispatch.png) 125 | 126 | 127 | ### 静态发布 128 | 静态发布的优势也显而易见,就是简单方便速度快,不需要从远端拿额外的资源,文章和代码一起打包发布。传统的 JS 生态会很容易做这件事,丰富的打包生态只需要一个插件来帮助解析 md 模块,就能在依赖图里见到它们了 :D。 129 | 130 | 但是如果是 Rust 的 wasm 生态,Trunk 要做这样的事儿并不容易,其根本是因为 Trunk 并没有提供类似 Webpack 插件的机制,直接导致无法介入 Trunk 的编译过程,这部分就必须手动来搞了,于是我又写了个 [CLI](https://github.com/zzhack-stack/reducer)... 131 | 132 | 其思路是先在本地写文章,然后把所有文章都聚合起来当作一个 rs 文件载入源码,再在 trunk 热更新的时候触发一下载入的动作,应用可以通过 service 来读取这部分的内容,就可以做到愉快的静态发布文章了。 133 | 134 | ![静态发布](../app/assets/sources/static_dispatch.png) 135 | 136 | 一个自动生成的 `posts.rs` 看起来像: 137 | 138 | ```rust 139 | use std::*; 140 | 141 | #[derive(Clone)] 142 | pub struct PostFile { 143 | pub content: &'static str, 144 | pub modified_time: u128 145 | } 146 | 147 | pub static POSTS: [PostFile; 1] = [ 148 | PostFile { 149 | content: include_str!("../../posts/build_blog.md"), 150 | modified_time: 1654596087967 151 | }, 152 | 153 | ]; 154 | ``` 155 | 156 | ## 聊聊 Rust 的开发体验 157 | 最后最后,聊聊 Rust 开发 Web 的体验,虽然在性能上已经碾压 JS,内存控制更是没话说,但是开发体验并不如直接用 JS Stack 来写 WEB,这需要你通过 Rust 的思维来写 UI 的逻辑。 158 | 159 | 但是 Rust 太精致(安全)了,但是 WEB 通常不需要那么精致。 160 | -------------------------------------------------------------------------------- /posts/mlog_2022-10-26.md: -------------------------------------------------------------------------------- 1 | ```metadata 2 | { 3 | "cover": "../app/assets/sources/mlog_cover.png", 4 | "tag": "碎碎念", 5 | "title": "飞贼" 6 | } 7 | ``` 8 | 9 | 早上匆匆茫茫去买肠粉,走的时候跑的飞快忘记给钱了😅,估计老板都懵了,拿了肠粉就跑... 10 | -------------------------------------------------------------------------------- /router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "router" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew-router = "0.16.0" 10 | -------------------------------------------------------------------------------- /router/src/lib.rs: -------------------------------------------------------------------------------- 1 | use yew_router::prelude::*; 2 | 3 | #[derive(Clone, Routable, PartialEq, Debug)] 4 | pub enum RootRoutes { 5 | #[at("/home")] 6 | Home, 7 | #[at("/posts/:filename")] 8 | Post { filename: String }, 9 | #[at("/")] 10 | Root, 11 | #[at("/projects")] 12 | Projects, 13 | #[at("/links")] 14 | Links, 15 | #[at("/about")] 16 | About, 17 | // Compatible with https://github.com/jetli/awesome-yew 18 | #[at("/technology")] 19 | Technology, 20 | #[not_found] 21 | #[at("/404")] 22 | NotFound, 23 | } 24 | -------------------------------------------------------------------------------- /services/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /services/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "services" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | pulldown-cmark = "0.8.0" 10 | regex = "1.5.5" 11 | serde = "1.0.137" 12 | web-sys = { version = "0.3", features = ["HtmlMetaElement", "Document", "Element", "DocumentFragment", "HtmlTemplateElement", "MediaQueryList"] } 13 | serde_json = "1.0.64" 14 | once_cell = "1.10.0" 15 | yew = "0.19.3" 16 | material-yew = { git = "https://github.com/hamza1311/material-yew", features = ["full"] } 17 | lazy_static = "1.4.0" 18 | chrono = "0.4" 19 | urlencoding = "2.1.0" 20 | wasm-logger = "0.2.0" 21 | log = "0.4.17" 22 | 23 | [dependencies.syntect] 24 | version = "4.5" 25 | default-features = false 26 | features = [ 27 | "html", 28 | "dump-load", 29 | "regex-fancy" 30 | ] 31 | 32 | [build-dependencies] 33 | syntect = { version = "4.5", default-features = false, features = ["default-fancy"] } 34 | -------------------------------------------------------------------------------- /services/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod links_service; 2 | pub mod markdown_service; 3 | pub mod post_service; 4 | pub mod posts; 5 | pub mod projects_service; 6 | pub mod theme_service; 7 | extern crate lazy_static; 8 | -------------------------------------------------------------------------------- /services/src/links_service/links.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": [ 3 | { 4 | "name": "Clay 的技术博客", 5 | "addr": "https://www.techgrow.cn", 6 | "desc": "用进废退 | 艺不压身", 7 | "logo": "https://www.techgrow.cn/img/head.jpg" 8 | }, 9 | { 10 | "name": "Christine的博客", 11 | "desc": "虽然我不够优秀,但我从未放弃过努力。", 12 | "logo": "https://christine-only.github.io/blog/logo.png", 13 | "addr": "https://christine-only.github.io/blog/" 14 | }, 15 | { 16 | "name": "Forever丿顾北博客", 17 | "addr": "https://forevergubei.gitee.io/myblod/", 18 | "desc": "一个追寻大佬脚步的小白", 19 | "logo": "https://forevergubei.gitee.io/myblod/logo.png" 20 | }, 21 | { 22 | "name": "MrDJun's Blog", 23 | "addr": "https://mrdjun.gitee.io/", 24 | "desc": "Just an ordinary JAVA programmer", 25 | "logo": "https://mrdjun.gitee.io/images/avatar.png" 26 | }, 27 | { 28 | "name": "Fatpandac’s Blog", 29 | "addr": "https://www.fatpandac.com/", 30 | "desc": "不知道写啥的个人博客", 31 | "logo": "https://avatars.githubusercontent.com/u/30423976?v=4" 32 | }, 33 | { 34 | "name": "低调小熊猫", 35 | "addr": "https://ilovey.live", 36 | "desc": "读万卷书,行万里路,赚很多钱", 37 | "logo": "https://uss.ilovey.live/img/avatar.jpg" 38 | }, 39 | { 40 | "name": "风吹鼍鼓旌旗动", 41 | "addr": "http://idinr.com/", 42 | "desc": "有技术,有科学", 43 | "logo": "https://s2.loli.net/2022/06/15/d6MYUaVWf74sKIi.jpg" 44 | }, 45 | { 46 | "name": "Levi's Blog", 47 | "addr": "https://www.jianshu.com/u/1fc674525eeb", 48 | "desc": "学的越多,懂的越少", 49 | "logo": "https://upload.jianshu.io/users/upload_avatars/10826765/5e06dfca-ff26-488c-a60c-79ca6504917a?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240" 50 | }, 51 | { 52 | "name": "高耳机", 53 | "addr": "http://www.ragnaroks.site/", 54 | "desc": "青春有爱 年少有梦", 55 | "logo": "https://avatars.githubusercontent.com/u/7598199?v=4" 56 | }, 57 | { 58 | "name": "Pseudoyu", 59 | "addr": "https://www.pseudoyu.com/", 60 | "desc": "Blockchain | Programming | Photography | Misty", 61 | "logo": "https://www.pseudoyu.com/images/author.webp" 62 | }, 63 | { 64 | "name": "circlehotarux's blog", 65 | "addr": "https://www.circlehotarux.me/", 66 | "desc": "心诚则灵", 67 | "logo": "https://avatars.githubusercontent.com/u/48060080?v=4" 68 | }, 69 | { 70 | "name": "小博's blog", 71 | "addr": "https://zhongbohuang.github.io/hzb-blog.github.io/", 72 | "desc": "记录知识,记录生活", 73 | "logo": "https://zhongbohuang.github.io/hzb-blog.github.io/img/avatar.png" 74 | }, 75 | { 76 | "name": "绝对值の垃圾站", 77 | "addr": "https://absolutevalue.cc", 78 | "desc": "一个垃圾站,写点垃圾话", 79 | "logo": "https://bu.dusays.com/2022/10/11/63456cf24d191.png" 80 | }, 81 | { 82 | "name": "SeerSu", 83 | "addr": "https://seersu.me", 84 | "desc": "留给自己一片大陆,把无垠的大海留给飞鸟和大鱼", 85 | "logo": "https://seersu.me/favicon/avatar.jpg" 86 | }, 87 | { 88 | "name": "Scorpio's Blog", 89 | "addr": "http://0925.wang", 90 | "desc": "改掉拖延症~", 91 | "logo": "https://q.qlogo.cn/headimg_dl?bs=qq&dst_uin=1849748550@qq.com&src_uin=qq.feixue.me&fid=blog&spec=100&id=1000" 92 | }, 93 | { 94 | "name": "一物一世界的 blog", 95 | "addr": "https://www.flyfrag.cn/", 96 | "desc": "太阳强烈,水波温柔", 97 | "logo": "https://avatars.githubusercontent.com/u/53332638?v=4" 98 | }, 99 | { 100 | "name": "Airing's blog", 101 | "addr": "https://blog.ursb.me/", 102 | "desc": "我是一只小小小小熊", 103 | "logo": "https://airing.ursb.me/image/airing-face.png" 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /services/src/links_service/links_service.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use serde::Deserialize; 3 | use serde_json; 4 | #[derive(Deserialize, Clone, PartialEq)] 5 | pub struct RawLinkData { 6 | pub links: Vec, 7 | } 8 | 9 | #[derive(Deserialize, Clone, PartialEq)] 10 | pub struct LinkData { 11 | pub name: String, 12 | pub desc: String, 13 | pub addr: String, 14 | pub logo: Option, 15 | } 16 | 17 | pub struct LinksService { 18 | links_data: Vec, 19 | } 20 | 21 | impl LinksService { 22 | pub fn new() -> LinksService { 23 | let links_data = include_str!("./links.json"); 24 | let links_data: RawLinkData = serde_json::from_str(links_data).unwrap(); 25 | 26 | LinksService { 27 | links_data: links_data.links, 28 | } 29 | } 30 | 31 | pub fn get_links_data(&self) -> Vec { 32 | self.links_data.clone() 33 | } 34 | } 35 | 36 | pub static LINKS_SERVICE: Lazy = Lazy::new(|| LinksService::new()); 37 | -------------------------------------------------------------------------------- /services/src/links_service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod links_service; 2 | -------------------------------------------------------------------------------- /services/src/markdown_service/elements.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use urlencoding::encode; 3 | 4 | #[derive(Serialize, Deserialize, Debug)] 5 | pub struct GitHubRenderBlock { 6 | pub url: String, 7 | pub repo: String, 8 | pub description: String, 9 | } 10 | 11 | pub fn render_heading(content: String, level: u32) -> String { 12 | format!( 13 | "{}", 14 | content, level, encode(content.as_str()).to_string(), content, level 15 | ) 16 | } 17 | 18 | pub fn render_code_block(code_block: String) -> String { 19 | format!( 20 | "
21 |
22 |
23 |
24 |
25 |
26 | {} 27 |
", 28 | code_block 29 | ) 30 | } 31 | 32 | pub fn render_image(url: String, alt: String) -> String { 33 | format!( 34 | "
35 | {} 36 | {} 37 |
", 38 | url, alt, url, alt 39 | ) 40 | } 41 | 42 | pub fn render_github_render_block(github_render_block: GitHubRenderBlock) -> String { 43 | format!( 44 | "", 58 | github_render_block.url, github_render_block.repo, github_render_block.description 59 | ) 60 | } 61 | 62 | pub fn render_spotlight(text: &str) -> String { 63 | format!( 64 | "
65 |
{}
66 |
", 67 | text 68 | ) 69 | } 70 | 71 | pub fn render_code(code: String) -> String { 72 | format!("{}", code) 73 | } 74 | -------------------------------------------------------------------------------- /services/src/markdown_service/mod.rs: -------------------------------------------------------------------------------- 1 | mod elements; 2 | pub mod markdown_service; 3 | -------------------------------------------------------------------------------- /services/src/post_service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod post_card_size; 2 | pub mod post_service; 3 | -------------------------------------------------------------------------------- /services/src/post_service/post_card_size.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Clone, Debug)] 2 | pub enum PostCardSize { 3 | Small, 4 | Large, 5 | } 6 | 7 | impl From for PostCardSize { 8 | fn from(size: String) -> Self { 9 | match size.as_str() { 10 | "small" => PostCardSize::Small, 11 | "large" => PostCardSize::Large, 12 | _ => PostCardSize::Small, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/src/post_service/post_service.rs: -------------------------------------------------------------------------------- 1 | use crate::markdown_service::markdown_service::{MarkdownService, PostMetadata}; 2 | use crate::posts::POSTS; 3 | use chrono::NaiveDateTime; 4 | use once_cell::sync::Lazy; 5 | use regex::Regex; 6 | use std::cmp::Ordering; 7 | 8 | #[derive(Clone, Debug, PartialEq)] 9 | pub struct Post { 10 | pub metadata: PostMetadata, 11 | pub raw_content: &'static str, 12 | pub desc: String, 13 | pub modified_time: String, 14 | pub filename: &'static str, 15 | } 16 | 17 | pub struct PostService { 18 | posts: Vec, 19 | } 20 | 21 | #[derive(Clone)] 22 | pub enum FilterTag { 23 | All, 24 | Tag(String), 25 | } 26 | 27 | const MAX_DESC_LENGTH: usize = 600; 28 | 29 | pub fn find_char_boundary(s: &str, index: usize) -> usize { 30 | if s.len() <= index { 31 | return index; 32 | } 33 | 34 | let mut new_index = index; 35 | while !s.is_char_boundary(new_index) { 36 | new_index += 1; 37 | } 38 | 39 | new_index 40 | } 41 | 42 | impl PostService { 43 | pub fn new() -> PostService { 44 | let posts = PostService::read_posts_into_memo(); 45 | 46 | PostService { posts } 47 | } 48 | 49 | pub fn get_posts(&self) -> Vec { 50 | self.posts.clone() 51 | } 52 | 53 | pub fn trim_useless_symbol(content: &'static str) -> String { 54 | Regex::new(r#"([\n]|```[^`]+```|`[^`]+`)"#) 55 | .unwrap() 56 | .replace_all(content, "") 57 | .into_owned() 58 | } 59 | 60 | pub fn find_post_by_filename(&self, filename: &str) -> Option { 61 | self.posts 62 | .clone() 63 | .into_iter() 64 | .find(|post| post.filename == filename) 65 | } 66 | 67 | pub fn get_tags(&self) -> Vec { 68 | let mut tags = vec![]; 69 | 70 | self.posts.iter().for_each(|post| { 71 | let is_exist = tags.contains(&post.metadata.tag); 72 | 73 | if !is_exist { 74 | tags.push(post.metadata.tag.clone()); 75 | } 76 | }); 77 | 78 | tags 79 | } 80 | 81 | pub fn filter_post_by_tag(&self, tag: FilterTag) -> Vec { 82 | let posts = self.posts.clone(); 83 | 84 | match tag { 85 | FilterTag::All => posts, 86 | FilterTag::Tag(tag) => posts 87 | .into_iter() 88 | .filter(|post| post.metadata.tag == tag) 89 | .collect::>(), 90 | } 91 | } 92 | 93 | fn read_posts_into_memo() -> Vec { 94 | let mut posts = POSTS 95 | .clone() 96 | .into_iter() 97 | .map(|post| { 98 | let markdown_service = MarkdownService::new(post.content.to_string()); 99 | let metadata = markdown_service.extract_metadata().expect( 100 | "Please make sure the post has metadata which declare using block syntax.", 101 | ); 102 | let parsed_content = PostService::trim_useless_symbol(post.content); 103 | let parsed_content_length = parsed_content.len(); 104 | let slice_desc_length = if parsed_content_length > MAX_DESC_LENGTH { 105 | MAX_DESC_LENGTH 106 | } else { 107 | parsed_content_length 108 | }; 109 | let desc = parsed_content[..find_char_boundary(&parsed_content, slice_desc_length)] 110 | .to_string(); 111 | let modified_secs = (post.modified_time / 1000) as i64; 112 | let modified_time = NaiveDateTime::from_timestamp(modified_secs, 0); 113 | let modified_time = modified_time.format("%a, %b %e %Y").to_string(); 114 | 115 | Post { 116 | metadata, 117 | raw_content: post.content, 118 | desc, 119 | modified_time, 120 | filename: post.filename, 121 | } 122 | }) 123 | .collect::>(); 124 | 125 | posts.sort_by(|a, b| { 126 | if a.metadata.pined { 127 | Ordering::Less 128 | } else if b.metadata.pined { 129 | Ordering::Greater 130 | } else { 131 | a.modified_time.cmp(&b.modified_time) 132 | } 133 | }); 134 | 135 | posts 136 | } 137 | } 138 | 139 | pub static POST_SERVICE: Lazy = Lazy::new(|| PostService::new()); 140 | -------------------------------------------------------------------------------- /services/src/posts.rs: -------------------------------------------------------------------------------- 1 | use std::*; 2 | 3 | #[derive(Clone)] 4 | pub struct PostFile { 5 | pub content: &'static str, 6 | pub modified_time: u128, 7 | pub filename: &'static str 8 | } 9 | 10 | pub static POSTS: [PostFile; 3] = [ 11 | PostFile { 12 | content: include_str!("../../posts/mlog_2022-10-26.md"), 13 | modified_time: 1666755490770, 14 | filename: "mlog_2022-10-26" 15 | }, 16 | PostFile { 17 | content: include_str!("../../posts/build_blog.md"), 18 | modified_time: 1654609915763, 19 | filename: "build_blog" 20 | }, 21 | PostFile { 22 | content: include_str!("../../posts/add_links.md"), 23 | modified_time: 1654855511389, 24 | filename: "add_links" 25 | }, 26 | 27 | ]; 28 | -------------------------------------------------------------------------------- /services/src/projects_service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod projects_service; 2 | -------------------------------------------------------------------------------- /services/src/projects_service/projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | { 4 | "name": "zzhack", 5 | "addr": "https://github.com/zzhack-stack/zzhack", 6 | "desc": "⚙️ 🦀️ My personal blog site" 7 | }, 8 | { 9 | "name": "Archie", 10 | "addr": "https://github.com/wizardoc/archie", 11 | "desc": "GraphQL API server for @wizardoc/wizard, powered by @gin, Git-based document management, timely notification for document changes." 12 | }, 13 | { 14 | "name": "Chelli", 15 | "addr": "https://github.com/mistricky/chelli", 16 | "desc": "🖖🏻Chelli is a function set that implements command-line parsing using Zsh Shell. And the name of Chelli means that combination Shell and CLI." 17 | }, 18 | { 19 | "name": "Arrow Cache", 20 | "addr": "https://github.com/wizardoc/arrow-cache", 21 | "desc": "🏹️Cache mechanism base on Web Worker, help us build high performance webApp." 22 | }, 23 | { 24 | "name": "postcss-relaxed-unit", 25 | "addr": "https://github.com/mistricky/postcss-relaxed-unit", 26 | "desc": "🍮Postcss-relaxed-unit is a postcss plugin for unit tranformation and make write css easier with custom unit." 27 | }, 28 | { 29 | "name": "Fence", 30 | "addr": "https://github.com/wizardoc/fence", 31 | "desc": "🐟Fence is an operator-based request library that provide a serises of APIs to help you more easily manipulate the request flow" 32 | }, 33 | { 34 | "name": "Wizard", 35 | "addr": "https://github.com/wizardoc/wizard", 36 | "desc": "🍳Wizardoc is a WEBAPP for managing documents and knowledge." 37 | }, 38 | { 39 | "name": "Flat", 40 | "addr": "https://github.com/mistlang/Flat", 41 | "desc": "🍪Lightweight MVVM framework base on TypeScript." 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /services/src/projects_service/projects_service.rs: -------------------------------------------------------------------------------- 1 | use crate::post_service::post_service::{Post, POST_SERVICE}; 2 | use once_cell::sync::Lazy; 3 | use serde::Deserialize; 4 | use serde_json; 5 | 6 | #[derive(Deserialize, Clone)] 7 | pub struct RawProject { 8 | pub name: String, 9 | pub desc: String, 10 | pub addr: String, 11 | pub post: Option, 12 | } 13 | 14 | #[derive(Clone, PartialEq)] 15 | pub struct Project { 16 | pub name: String, 17 | pub desc: String, 18 | pub addr: String, 19 | pub post: Option, 20 | } 21 | 22 | #[derive(Deserialize, Clone)] 23 | pub struct ProjectsData { 24 | pub projects: Vec, 25 | } 26 | 27 | pub struct ProjectsService { 28 | projects: Vec, 29 | } 30 | 31 | impl ProjectsService { 32 | pub fn new() -> ProjectsService { 33 | let projects_data = include_str!("./projects.json"); 34 | let projects_data: ProjectsData = serde_json::from_str(projects_data).unwrap(); 35 | let projects = projects_data 36 | .projects 37 | .iter() 38 | .map(|raw_project| { 39 | let post = match raw_project.post.clone() { 40 | Some(filename) => POST_SERVICE.find_post_by_filename(&filename).clone(), 41 | None => None, 42 | }; 43 | 44 | Project { 45 | addr: raw_project.addr.clone(), 46 | name: raw_project.name.clone(), 47 | desc: raw_project.desc.clone(), 48 | post, 49 | } 50 | }) 51 | .collect(); 52 | 53 | ProjectsService { projects } 54 | } 55 | 56 | pub fn get_projects(&self) -> Vec { 57 | self.projects.clone() 58 | } 59 | 60 | pub fn get_projects_by_odd_even(&self) -> (Vec, Vec) { 61 | let mut even = vec![]; 62 | let mut odd = vec![]; 63 | 64 | for (i, project) in self.projects.iter().enumerate() { 65 | if (i + 1) % 2 == 0 { 66 | even.push(project.clone()); 67 | } else { 68 | odd.push(project.clone()); 69 | } 70 | } 71 | 72 | (odd, even) 73 | } 74 | } 75 | 76 | pub static PROJECTS_SERVICE: Lazy = Lazy::new(|| ProjectsService::new()); 77 | -------------------------------------------------------------------------------- /services/src/theme_service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod theme; 2 | pub mod theme_service; 3 | 4 | pub use theme::*; 5 | pub use theme_service::*; 6 | -------------------------------------------------------------------------------- /services/src/theme_service/theme.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, PartialEq, Debug)] 2 | pub enum Theme { 3 | Dark, 4 | Light, 5 | Auto, 6 | } 7 | 8 | impl Theme { 9 | pub fn into_str(&self) -> &'static str { 10 | match self { 11 | Theme::Dark => "dark", 12 | Theme::Light => "light", 13 | Theme::Auto => "auto", 14 | } 15 | } 16 | 17 | pub fn from(theme: &str) -> Theme { 18 | match theme { 19 | "dark" => Theme::Dark, 20 | "light" => Theme::Light, 21 | "auto" => Theme::Auto, 22 | _ => Theme::Dark, 23 | } 24 | } 25 | 26 | pub fn eq(&self, other: &Theme) -> bool { 27 | self.clone().into_str() == other.clone().into_str() 28 | } 29 | 30 | pub fn nq(&self, other: &Theme) -> bool { 31 | !self.eq(other) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /services/src/theme_service/theme_service.rs: -------------------------------------------------------------------------------- 1 | use super::theme::Theme; 2 | use once_cell::sync::Lazy; 3 | 4 | pub struct ThemePool { 5 | pub theme: Theme, 6 | } 7 | 8 | pub struct ThemeService { 9 | pool: &'static mut Lazy, 10 | } 11 | 12 | const THEME_KEY: &'static str = "THEME"; 13 | 14 | impl ThemeService { 15 | pub fn from_storage() -> ThemeService { 16 | let instance = unsafe { 17 | ThemeService { 18 | pool: &mut THEME_POOL, 19 | } 20 | }; 21 | 22 | instance.mount_on_dom(); 23 | instance 24 | } 25 | 26 | pub fn mount_on_dom(&self) { 27 | let window = web_sys::window().unwrap(); 28 | let document = window.document().unwrap(); 29 | let body = document.body().unwrap(); 30 | 31 | body.set_class_name(self.pool.theme.clone().into_str()); 32 | } 33 | pub fn get_theme_follow_os_from_storage() -> Theme { 34 | let is_dark_theme = web_sys::window() 35 | .unwrap() 36 | .match_media("(prefers-color-scheme: dark)") 37 | .unwrap() 38 | .unwrap() 39 | .matches(); 40 | 41 | if is_dark_theme { 42 | Theme::Dark 43 | } else { 44 | Theme::Light 45 | } 46 | } 47 | 48 | pub fn convert_auto_to_actually_theme(theme: Theme) -> Theme { 49 | match theme { 50 | Theme::Auto => ThemeService::get_theme_follow_os_from_storage(), 51 | _ => theme, 52 | } 53 | } 54 | 55 | pub fn get_theme_from_storage() -> Theme { 56 | let local_storage = web_sys::window().unwrap().local_storage().unwrap().unwrap(); 57 | 58 | match local_storage.get_item(THEME_KEY).unwrap() { 59 | Some(theme_literal) => Theme::from(&theme_literal), 60 | None => { 61 | local_storage 62 | .set_item(THEME_KEY, Theme::Dark.into_str()) 63 | .unwrap(); 64 | 65 | Theme::Dark 66 | } 67 | } 68 | } 69 | 70 | pub fn get_theme(&self) -> &Theme { 71 | &self.pool.theme 72 | } 73 | 74 | fn update_theme(&mut self) { 75 | let theme = ThemeService::get_theme_from_storage(); 76 | self.pool.theme = ThemeService::convert_auto_to_actually_theme(theme); 77 | self.mount_on_dom(); 78 | } 79 | 80 | pub fn set_theme(&mut self, theme: &Theme) { 81 | let local_storage = web_sys::window().unwrap().local_storage().unwrap().unwrap(); 82 | let stringify_theme = theme.clone().into_str(); 83 | 84 | local_storage.set_item(THEME_KEY, stringify_theme).unwrap(); 85 | self.update_theme(); 86 | } 87 | } 88 | 89 | pub static mut THEME_POOL: Lazy = Lazy::new(|| { 90 | let theme = 91 | ThemeService::convert_auto_to_actually_theme(ThemeService::get_theme_from_storage()); 92 | 93 | ThemePool { theme } 94 | }); 95 | -------------------------------------------------------------------------------- /templates/md_parser.template: -------------------------------------------------------------------------------- 1 | use std::*; 2 | 3 | #[derive(Clone)] 4 | pub struct PostFile { 5 | pub content: &'static str, 6 | pub modified_time: u128, 7 | pub filename: &'static str 8 | } 9 | 10 | pub static POSTS: [PostFile; {{TRAVERSE_COUNT}}] = [ 11 | {{TEMPLATE}} 12 | ]; 13 | -------------------------------------------------------------------------------- /templates/md_parser_iteration.template: -------------------------------------------------------------------------------- 1 | PostFile { 2 | content: include_str!("../../posts/{{STEM}}.md"), 3 | modified_time: {{MODIFY_TIME}}, 4 | filename: "{{STEM}}" 5 | }, 6 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ui" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yew = "0.19.3" 10 | stylist = {version = "0.10", features = ["yew_integration"]} 11 | utils = {path = "../utils"} 12 | global = {path = "../global"} 13 | services = {path = "../services"} 14 | web-sys = { version = "0.3", features = ["HtmlMetaElement", "Document", "Element", "DocumentFragment", "HtmlTemplateElement", "MediaQueryList"] } 15 | wasm-logger = "0.2.0" 16 | log = "0.4.17" 17 | material-yew = { git = "https://github.com/hamza1311/material-yew", features = ["full"] } 18 | urlencoding = "2.1.0" 19 | router = {path = "../router"} 20 | yew-router = "0.16.0" 21 | -------------------------------------------------------------------------------- /ui/src/common/contact.rs: -------------------------------------------------------------------------------- 1 | use super::image::Icon; 2 | use crate::link::Link; 3 | use utils::use_style; 4 | use yew::prelude::*; 5 | 6 | #[derive(Properties, Clone, PartialEq)] 7 | pub struct ContactsProps { 8 | pub source: Vec, 9 | } 10 | 11 | #[derive(Clone, PartialEq)] 12 | pub enum ContactType { 13 | Twitter, 14 | GitHub, 15 | Email, 16 | WeChat, 17 | Discord, 18 | } 19 | 20 | impl From<&ContactType> for &'static str { 21 | fn from(contact: &ContactType) -> &'static str { 22 | match contact { 23 | ContactType::Discord => "discord.svg", 24 | ContactType::Twitter => "twitter.svg", 25 | ContactType::WeChat => "wechat.svg", 26 | ContactType::Email => "gmail.svg", 27 | ContactType::GitHub => "github.svg", 28 | } 29 | } 30 | } 31 | 32 | impl ContactType { 33 | pub fn into_lnk(&self) -> &'static str { 34 | match self { 35 | ContactType::Discord => "#", 36 | ContactType::Twitter => "https://twitter.com/_mistricky", 37 | ContactType::WeChat => "#", 38 | ContactType::Email => "mailto:mist.zzh@gmail.com", 39 | ContactType::GitHub => "https://github.com/mistricky", 40 | } 41 | } 42 | } 43 | 44 | impl ContactType { 45 | fn has_theme(&self) -> bool { 46 | match self { 47 | ContactType::GitHub => true, 48 | _ => false, 49 | } 50 | } 51 | 52 | fn into_size(&self) -> i32 { 53 | match self { 54 | ContactType::GitHub => 30, 55 | _ => 32, 56 | } 57 | } 58 | } 59 | 60 | #[function_component(Contacts)] 61 | pub fn contacts(props: &ContactsProps) -> Html { 62 | let style = use_style!( 63 | r" 64 | display: flex; 65 | align-items: center; 66 | " 67 | ); 68 | 69 | let render_contacts = props 70 | .source 71 | .iter() 72 | .map(|contact| { 73 | let source: &'static str = contact.into(); 74 | html! { 75 | 76 | 77 | 78 | } 79 | }) 80 | .collect::(); 81 | 82 | html! { 83 |
84 | {render_contacts} 85 |
86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ui/src/common/container.rs: -------------------------------------------------------------------------------- 1 | use crate::use_style; 2 | use yew::prelude::*; 3 | 4 | #[derive(Properties, Clone, Debug, PartialEq)] 5 | pub struct ContainerProps { 6 | pub children: Children, 7 | } 8 | 9 | #[function_component(Container)] 10 | pub fn container(props: &ContainerProps) -> Html { 11 | let style = use_style!( 12 | r" 13 | width: 100%; 14 | display: flex; 15 | justify-content: center; 16 | 17 | .container-box { 18 | width: 816px; 19 | box-sizing: border-box; 20 | } 21 | 22 | @media (max-width: 600px) { 23 | .container-box { 24 | min-width: 100%; 25 | width: 100%; 26 | padding: 0 22px; 27 | } 28 | } 29 | " 30 | ); 31 | 32 | html! { 33 |
34 |
35 | {props.children.clone()} 36 |
37 |
38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/common/footer.rs: -------------------------------------------------------------------------------- 1 | use crate::contact::{ContactType, Contacts}; 2 | use crate::container::Container; 3 | use utils::use_style; 4 | use yew::prelude::*; 5 | 6 | #[function_component(Footer)] 7 | pub fn footer() -> Html { 8 | let style = use_style!( 9 | r" 10 | width: 100%; 11 | background: var(--base-color); 12 | padding-bottom: 18px; 13 | 14 | .contacts { 15 | margin-top: 31px; 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | } 20 | 21 | .copyright { 22 | display: flex; 23 | justify-content: center; 24 | } 25 | 26 | .text { 27 | font-size: 14px; 28 | } 29 | 30 | @media (max-width: 600px) { 31 | .contacts { 32 | flex-direction: column; 33 | height: auto; 34 | padding-bottom: 30px; 35 | } 36 | } 37 | " 38 | ); 39 | 40 | html! { 41 |
42 | 43 |
44 |
45 |
46 |
{"Powered by Rust & Yew"}
47 |
{"Illustration by Icons 8 from Ouch!"}
48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/common/gradient_title.rs: -------------------------------------------------------------------------------- 1 | use stylist::style; 2 | use yew::prelude::*; 3 | 4 | #[derive(Properties, Clone, PartialEq)] 5 | pub struct GradientTitleProps { 6 | pub children: Children, 7 | } 8 | 9 | #[function_component(GradientTitle)] 10 | pub fn gradient_title(props: &GradientTitleProps) -> Html { 11 | let style = style!( 12 | r" 13 | display: flex; 14 | margin-bottom: 21px; 15 | 16 | .gradient-title__content { 17 | font-size: 29px; 18 | position: relative; 19 | } 20 | 21 | .gradient-title__content::before { 22 | content: ''; 23 | width: 120%; 24 | display: block; 25 | height: 21px; 26 | border-radius: 100px; 27 | background: linear-gradient(90deg, #FF4AA8 0%, #F95D66 22%, #FE9C76 100%); 28 | position: absolute; 29 | z-index: -1; 30 | bottom: 0px; 31 | left: -5%; 32 | } 33 | " 34 | ) 35 | .unwrap(); 36 | 37 | html! { 38 |
39 |
40 | { props.children.clone() } 41 |
42 |
43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/common/header/drawer.rs: -------------------------------------------------------------------------------- 1 | use crate::header::drawer_item::DrawerItem; 2 | use stylist::{css, yew::styled_component}; 3 | use utils::html::render_with_insert_node; 4 | use yew::prelude::*; 5 | 6 | #[derive(Properties, Clone, PartialEq)] 7 | pub struct DrawerProps { 8 | pub is_open: UseStateHandle, 9 | pub children: ChildrenWithProps, 10 | } 11 | 12 | #[styled_component(Drawer)] 13 | pub fn drawer(props: &DrawerProps) -> Html { 14 | let style = css!( 15 | r" 16 | width: 100%; 17 | transition: all 0.2s; 18 | position: absolute; 19 | left: 0; 20 | background: var(--underlay-color); 21 | z-index: 5; 22 | transform: translateY(${translate}); 23 | 24 | .drawer-items { 25 | position: relative; 26 | z-index: 5; 27 | background: var(--underlay-color); 28 | padding: 0 20px; 29 | } 30 | 31 | .line { 32 | width: 100%; 33 | height: 1px; 34 | background: var(--shallow-gray); 35 | } 36 | ", 37 | translate = if *props.is_open { "56px" } else { "-100%" }, 38 | ); 39 | let mask_style = css!( 40 | r" 41 | top: 0; 42 | left: 0; 43 | position: fixed; 44 | width: 100%; 45 | height: 100vh; 46 | background: var(--mask-color); 47 | z-index: 4; 48 | display: ${display}; 49 | ", 50 | display = if *props.is_open { "block" } else { "none" } 51 | ); 52 | let render_nodes = props 53 | .children 54 | .iter() 55 | .map(|item| html! {{item}}) 56 | .collect::>(); 57 | let handle_mask_click = { 58 | let is_open = props.is_open.clone(); 59 | 60 | Callback::from(move |_| is_open.set(!*is_open)) 61 | }; 62 | 63 | html! { 64 | <> 65 |
66 |
67 |
68 | {render_with_insert_node(&render_nodes, &html! { 69 |
70 | })} 71 |
72 |
73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/common/header/drawer_item.rs: -------------------------------------------------------------------------------- 1 | use crate::link::Link; 2 | use router::RootRoutes; 3 | use utils::use_style; 4 | use yew::prelude::*; 5 | 6 | #[derive(Properties, Clone, PartialEq)] 7 | pub struct DrawerProps { 8 | pub lnk: RootRoutes, 9 | pub children: Children, 10 | } 11 | 12 | #[function_component(DrawerItem)] 13 | pub fn drawer_item(props: &DrawerProps) -> Html { 14 | let style = use_style!( 15 | r" 16 | padding: 12px 0; 17 | " 18 | ); 19 | 20 | html! { 21 |
22 | {props.children.clone()} 23 |
24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/common/header/header.rs: -------------------------------------------------------------------------------- 1 | use crate::contact::ContactType; 2 | use crate::container::Container; 3 | use crate::header::drawer::Drawer; 4 | use crate::header::drawer_item::DrawerItem; 5 | use crate::image::{Icon, ThemeImage}; 6 | use crate::link::Link; 7 | use crate::modal::{modal::Modal, modal_content::ModalContent}; 8 | use crate::theme_selector::ThemeSelector; 9 | use router::RootRoutes; 10 | use utils::resource::with_assets; 11 | use utils::theme::only_render_on_mobile; 12 | use utils::use_style; 13 | use yew::prelude::*; 14 | 15 | #[function_component(Header)] 16 | pub fn header() -> Html { 17 | let style = use_style!( 18 | r" 19 | width: 100%; 20 | 21 | .wrapper { 22 | height: 56px; 23 | justify-content: space-between; 24 | } 25 | 26 | .wrapper, .tabs, .left, .right, .setting-icon { 27 | display: flex; 28 | align-items: center; 29 | } 30 | 31 | .left img { 32 | height: 23px; 33 | display: flex; 34 | } 35 | 36 | .tabs { 37 | margin-left: 88px; 38 | } 39 | 40 | .tabs a { 41 | font-size: 14px; 42 | margin: 0 15px; 43 | } 44 | 45 | .setting-icon { 46 | margin-right: 19px; 47 | } 48 | 49 | .header { 50 | height:56px; 51 | width: 100%; 52 | background: var(--base-color); 53 | position: relative; 54 | z-index: 6; 55 | } 56 | 57 | @media (max-width: 600px) { 58 | .tabs { 59 | display: none; 60 | } 61 | } 62 | " 63 | ); 64 | let control_theme_style = use_style!( 65 | r" 66 | display: flex; 67 | flex-direction: column; 68 | align-items: center; 69 | 70 | .control-theme__text { 71 | width: 100%; 72 | text-align: left; 73 | font-size: 14px; 74 | color: var(--sub-text-color); 75 | } 76 | 77 | .control-theme__img { 78 | width: 126px; 79 | } 80 | 81 | @media (max-width: 600px) { 82 | .control-theme__text { 83 | font-size: 12px; 84 | } 85 | } 86 | " 87 | ); 88 | let is_open_drawer_handle = use_state_eq(|| false); 89 | let is_open_theme_modal = use_state_eq(|| false); 90 | let handle_drawer_click = { 91 | let is_open_drawer_handle = is_open_drawer_handle.clone(); 92 | 93 | Callback::from(move |_| is_open_drawer_handle.set(!*is_open_drawer_handle)) 94 | }; 95 | let handle_setting_click = { 96 | let is_open_theme_modal = is_open_theme_modal.clone(); 97 | 98 | Callback::from(move |_| is_open_theme_modal.set(!*is_open_theme_modal)) 99 | }; 100 | 101 | html! { 102 |
103 | 104 | {"Posts"} 105 | {"Projects"} 106 | {"About"} 107 | {"Links"} 108 | 109 |
110 | 111 |
112 |
113 | 114 | 115 | 116 |
117 | {"Posts"} 118 | {"Projects"} 119 | {"About"} 120 | {"Links"} 121 |
122 |
123 |
124 | 125 | 126 | 127 | 128 | {only_render_on_mobile(html! { 129 | 130 | })} 131 |
132 |
133 |
134 |
135 | 136 | 137 | 138 |
139 |

{"你可以在任何地方通过开关随时修改你的主题"}

140 | 141 |
142 |
143 |
144 |
145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ui/src/common/header/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod drawer; 2 | pub mod drawer_item; 3 | pub mod header; 4 | pub use header::Header; 5 | -------------------------------------------------------------------------------- /ui/src/common/image.rs: -------------------------------------------------------------------------------- 1 | use global::theme_context::ThemeContext; 2 | use material_yew::MatIconButton; 3 | use stylist::{style, yew::styled_component}; 4 | use utils::resource::{with_assets, with_assets_by_theme}; 5 | use utils::theme::with_reactive_source; 6 | use yew::prelude::*; 7 | 8 | #[derive(Properties, Clone, PartialEq)] 9 | pub struct ImageProps { 10 | pub source: &'static str, 11 | } 12 | 13 | #[derive(Properties, Clone, PartialEq)] 14 | pub struct ThemeImageProps { 15 | #[prop_or(false)] 16 | pub is_reactive: bool, 17 | pub source: &'static str, 18 | #[prop_or(String::from(""))] 19 | pub style: String, 20 | } 21 | 22 | #[derive(Properties, Clone, PartialEq)] 23 | pub struct IconProps { 24 | pub source: &'static str, 25 | #[prop_or(true)] 26 | pub has_theme: bool, 27 | pub size: i32, 28 | #[prop_or(String::from(""))] 29 | pub style: String, 30 | #[prop_or_default] 31 | pub onclick: Option>, 32 | } 33 | 34 | #[derive(Properties, Clone, PartialEq)] 35 | pub struct BaseImageProps { 36 | pub source: &'static str, 37 | #[prop_or(false)] 38 | pub has_theme: bool, 39 | #[prop_or(false)] 40 | pub is_reactive: bool, 41 | #[prop_or(String::from(""))] 42 | pub style: String, 43 | } 44 | 45 | #[function_component(BaseImage)] 46 | pub fn base_image(props: &BaseImageProps) -> Html { 47 | let theme_ctx = use_context::().unwrap(); 48 | let source = if props.is_reactive { 49 | with_reactive_source(props.source.to_string()) 50 | } else { 51 | props.source.to_string() 52 | }; 53 | let source = if props.has_theme { 54 | with_assets_by_theme(&source, &theme_ctx.theme) 55 | } else { 56 | with_assets(&source) 57 | }; 58 | 59 | html! { 60 | 61 | } 62 | } 63 | 64 | #[function_component(Image)] 65 | pub fn image(props: &ImageProps) -> Html { 66 | html! { 67 | 68 | } 69 | } 70 | 71 | #[function_component(ThemeImage)] 72 | pub fn theme_image(props: &ThemeImageProps) -> Html { 73 | html! { 74 | 75 | } 76 | } 77 | 78 | #[styled_component(Icon)] 79 | pub fn icon(props: &IconProps) -> Html { 80 | let style = style!( 81 | r" 82 | width: ${size}px; 83 | height: ${size}px; 84 | ", 85 | size = props.size, 86 | ) 87 | .unwrap(); 88 | let style = style.get_class_name(); 89 | let wrapper_style = style!( 90 | r" 91 | --mdc-icon-size: ${size}px; 92 | ", 93 | size = props.size, 94 | ) 95 | .unwrap(); 96 | let onclick_callback = match props.onclick.clone() { 97 | Some(callback) => callback, 98 | None => Callback::noop(), 99 | }; 100 | 101 | html! { 102 |
103 | 104 | 105 | 106 |
107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ui/src/common/layout.rs: -------------------------------------------------------------------------------- 1 | use super::{footer::Footer, header::Header}; 2 | use crate::common::switch::ThemeSwitchBar; 3 | use crate::container::Container; 4 | use utils::use_style; 5 | use yew::prelude::*; 6 | 7 | #[derive(Properties, PartialEq)] 8 | pub struct BaseLayoutProps { 9 | pub children: Children, 10 | } 11 | 12 | #[function_component(BaseLayout)] 13 | pub fn base_layout(props: &BaseLayoutProps) -> Html { 14 | let style = use_style!( 15 | r" 16 | width: 100%; 17 | height: 100%; 18 | position: relative; 19 | 20 | .theme-switch-bar { 21 | position: absolute; 22 | right: -118px; 23 | top: 63px; 24 | } 25 | 26 | .page-outlet { 27 | min-height: calc(100vh - 100px); 28 | } 29 | 30 | @media (max-width: 600px) { 31 | .theme-switch-bar { 32 | position: static; 33 | } 34 | } 35 | " 36 | ); 37 | 38 | html! { 39 | <> 40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | { props.children.clone() } 48 |
49 |
50 |
51 |