├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── backend ├── .env.example ├── Cargo.toml ├── README.md └── src │ ├── articles │ ├── mod.rs │ ├── models.rs │ └── services.rs │ ├── categories │ ├── mod.rs │ ├── models.rs │ └── services.rs │ ├── dbs │ ├── mod.rs │ └── mongo.rs │ ├── gql │ ├── mod.rs │ ├── mutations.rs │ └── queries.rs │ ├── main.rs │ ├── topics │ ├── mod.rs │ ├── models.rs │ └── services.rs │ ├── users │ ├── mod.rs │ ├── models.rs │ └── services.rs │ └── util │ ├── common.rs │ ├── constant.rs │ ├── cred.rs │ └── mod.rs ├── data ├── graphiql.jpg ├── handlebars.png ├── surfer-dev.sql └── yew.png ├── frontend-handlebars ├── .env.example ├── Cargo.toml ├── README.md ├── graphql │ ├── article_index.graphql │ ├── article_new.graphql │ ├── articles_list.graphql │ ├── index.graphql │ ├── register.graphql │ ├── schema.graphql │ ├── sign_in.graphql │ ├── topic_article_new.graphql │ ├── topics_new.graphql │ ├── user_dashboard.graphql │ ├── user_index.graphql │ ├── user_info.graphql │ └── users_list.graphql ├── scripts │ ├── rhai-repl.rs │ ├── rhai-run.rs │ ├── sci-format.rhai │ ├── str-trc.rhai │ ├── value-check.rhai │ └── website-svg.rhai ├── src │ ├── main.rs │ ├── models │ │ ├── articles.rs │ │ ├── mod.rs │ │ └── users.rs │ ├── routes │ │ ├── articles.rs │ │ ├── home.rs │ │ ├── mod.rs │ │ └── users.rs │ └── util │ │ ├── common.rs │ │ ├── constant.rs │ │ ├── mod.rs │ │ └── str_trait.rs ├── static │ ├── articles │ │ └── README.md │ ├── css │ │ ├── bootstrap.min.css │ │ ├── kw-t.css │ │ ├── night-owl.min.css │ │ ├── sidebar.css │ │ ├── sign.css │ │ ├── stacks-editor.min.css │ │ ├── stacks.min.css │ │ └── style.css │ ├── favicon.png │ ├── favicon.svg │ ├── imgs │ │ ├── rust-shijian.png │ │ ├── white.svg │ │ └── x.svg │ ├── js │ │ ├── bootstrap.bundle.min.js │ │ ├── highlight.min.js │ │ ├── jquery-3.6.0.min.js │ │ ├── kw-t.js │ │ └── stacks-editor.bundle.js │ ├── logo.png │ └── users │ │ └── README.md └── templates │ ├── ads.txt │ ├── articles │ ├── index.html │ ├── input.html │ └── list.html │ ├── common │ ├── elsewhere.html │ ├── footer.html │ ├── head.html │ ├── header.html │ ├── introduction.html │ ├── nav.html │ ├── pagination.html │ ├── sidebar.html │ └── topic.html │ ├── index.html │ ├── register.html │ ├── sign-in.html │ └── users │ ├── dashboard.html │ ├── index.html │ ├── list.html │ └── register.html ├── frontend-yew ├── .env.toml.example ├── Cargo.toml ├── README.md ├── assets │ ├── ccss │ │ ├── stacks.min.css │ │ └── style.css │ ├── css │ │ └── night-owl.min.css │ ├── imgs │ │ ├── favicons │ │ │ ├── apple-touch-icon.png │ │ │ ├── apple-touch-icon@2x.png │ │ │ └── favicon.ico │ │ ├── logos │ │ │ ├── open-graph.png │ │ │ ├── rusthub-w.png │ │ │ └── rusthub.png │ │ ├── rust-shijian.png │ │ ├── white.svg │ │ └── x.svg │ └── js │ │ ├── feature.banners.js │ │ ├── feature.darkmode.js │ │ ├── feature.notices.js │ │ ├── hamburger.js │ │ ├── highlight.min.js │ │ ├── hl.js │ │ ├── library.jquery.js │ │ ├── load.js │ │ ├── nav.selected.js │ │ ├── navigation.js │ │ ├── search.js │ │ ├── stacks.min.js │ │ └── theme.js ├── graphql │ ├── article.graphql │ ├── articles.graphql │ ├── author.graphql │ ├── categories.graphql │ ├── category.graphql │ ├── home.graphql │ ├── register.graphql │ ├── schema.graphql │ ├── sign_in.graphql │ ├── topic.graphql │ └── topics.graphql ├── index.html ├── src │ ├── components │ │ ├── footer.rs │ │ ├── header.rs │ │ ├── mod.rs │ │ └── nodes.rs │ ├── main.rs │ ├── manage │ │ ├── manage_index.rs │ │ └── mod.rs │ ├── show │ │ ├── article.rs │ │ ├── articles.rs │ │ ├── author.rs │ │ ├── categories.rs │ │ ├── category.rs │ │ ├── explore.rs │ │ ├── home.rs │ │ ├── mod.rs │ │ ├── register.rs │ │ ├── sign_in.rs │ │ ├── topic.rs │ │ └── topics.rs │ └── util │ │ ├── common.rs │ │ ├── constant.rs │ │ ├── mod.rs │ │ ├── models.rs │ │ └── routes.rs └── trunk.toml └── rustfmt.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{graphql,sql,toml}] 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.rs] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.swp 4 | 5 | Cargo.lock 6 | target 7 | *.rs.bk 8 | 9 | .env 10 | .env.toml 11 | examples 12 | dist 13 | 14 | __pycache__ 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "html.format.endWithNewline": true, 3 | "html.format.indentHandlebars": true, 4 | "html.format.indentInnerHtml": true 5 | } 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["./backend", "./frontend-handlebars", "./frontend-yew"] 3 | resolver = "2" 4 | 5 | [profile.dev] 6 | split-debuginfo = "unpacked" 7 | 8 | [profile.release] 9 | # panic = "abort" 10 | codegen-units = 1 11 | opt-level = "z" 12 | lto = true 13 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 云上于天 zzy ask@ruonou.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # surfer 2 | 3 | The simple **WIP Blog** built on *pure Rust stack*, with upcoming upgrades. 4 | 5 | Backend for graphql services using tide, async-graphql, jsonwebtoken, mongodb and so on. 6 | 7 | There are two options for web frontend: 8 | - Frontend-yew for web application using yew, graphql_client, cookie and so on. 9 | - Frontend-handlebars for web application using tide, yew, rhai, surf, graphql_client, handlebars-rust, cookie and so on. 10 | 11 | See also: 12 | - https://github.com/zzy/tide-async-graphql-mongodb - Clean boilerplate for graphql services, wasm/yew & handlebars frontend. 13 | - https://github.com/piexue/piexue.com - Multi-language CMS based on the Rust web stacks. 14 | 15 | ## Features 16 | 17 | Demo site: 18 | - [niqin.com - NiQin Books Platform | 泥芹书馆](https://niqin.com) 19 | - [piexue.com - Project Matchmaking | 项目对接](https://piexue.com) 20 | 21 | ## MongoDB data 22 | 23 | MongoDB data(include structure & documents) file is `/data/surfer-dev.sql`. 24 | 25 | If you need mongodb cloud count, please send email to me. 26 | 27 | ## Stacks 28 | 29 | - [Rust](https://github.com/rust-lang/rust) - [Rust By Example](https://rust-by-example.niqin.com) and [Cargo Book](https://cargo.niqin.com) 30 | - [Tide](https://crates.io/crates/tide) - [Tide Book](https://tide-book.niqin.com) 31 | - [rhai](https://crates.io/crates/rhai) - [Embedded Scripting for Rust](https://rhai-script.niqin.com) 32 | - [async-graphql](https://crates.io/crates/async-graphql) - [async-graphql docs](https://async-graphql.niqin.com) 33 | - [mongodb & mongo-rust-driver](https://crates.io/crates/mongodb) 34 | - [Surf](https://crates.io/crates/surf) 35 | - [graphql_client](https://crates.io/crates/graphql_client) 36 | - [yew](https://yew.niqin.com) 37 | - [handlebars-rust](https://crates.io/crates/handlebars) 38 | - [jsonwebtoken](https://crates.io/crates/jsonwebtoken) 39 | - [cookie-rs](https://crates.io/crates/cookie) 40 | 41 | ## How to Build & Run? 42 | 43 | Please read: 44 | 45 | - [**Backend: graphql servies server**](./backend/README.md) 46 | - [**Frontend-yew: web application server**](./frontend-yew/README.md) 47 | - [**Frontend-handlebars: web application server**](./frontend-handlebars/README.md) 48 | 49 | ## Contributing 50 | 51 | You are welcome in contributing to the surfer project. 52 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | ADDR=127.0.0.1 2 | PORT=8000 3 | 4 | SITE_KEY=NALnvA++OlmRAiO2h..... # Replace with your SITE_KEY 5 | CLAIM_EXP=10000000000 6 | 7 | GQL_URI=gql 8 | GQL_VER=v1 9 | GIQL_VER=v1i 10 | 11 | MONGODB_URI=mongodb://surfer:surfer@127.0.0.1:27017 12 | MONGODB_NAME=ruonou_blog 13 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surfer-backend" 3 | version = "0.0.1" 4 | authors = ["zzy <9809920@qq.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | futures = "0.3" 9 | async-std = { path = "../../../crates/async-std", features = ["attributes"] } 10 | tide = { path = "../../../crates/tide", features = ["logger"] } 11 | 12 | dotenv = "0.15" 13 | lazy_static = "1.4" 14 | regex = "1.5" 15 | 16 | async-graphql = { version = "4.0.0-alpha", features = ["bson", "chrono"] } 17 | mongodb = { version = "2.2", default-features = false, features = [ 18 | "async-std-runtime", 19 | ] } 20 | 21 | serde = { version = "1.0", features = ["derive"] } 22 | chrono = "0.4" 23 | jsonwebtoken = "8.1" 24 | ring = "0.16" 25 | base64 = "0.13" 26 | 27 | deunicode = "1.3" 28 | pulldown-cmark = { version = "0.9", default-features = false, features = [ 29 | "simd", 30 | ] } 31 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Graphql Services Server - tide + async-graphql 2 | 3 | ## MongoDB data 4 | 5 | MongoDB data(include structure & documents) file is `/data/surfer-dev.sql`. 6 | 7 | If you need mongodb cloud count, please send email to me. 8 | 9 | ## Build & run 10 | 11 | ``` Bash 12 | git clone https://github.com/zzy/surfer.git 13 | cd surfer 14 | cargo build 15 | 16 | cd backend 17 | ``` 18 | 19 | Rename file `.env.example` to `.env`, or put the environment variables into a `.env` file: 20 | 21 | ``` 22 | ADDR=127.0.0.1 23 | PORT=8000 24 | 25 | SITE_KEY=NALnvA++OlmRAiO2h..... # Replace with your SITE_KEY 26 | CLAIM_EXP=10000000000 27 | 28 | GQL_URI=gql 29 | GQL_VER=v1 30 | GIQL_VER=v1i 31 | 32 | MONGODB_URI=mongodb://mongo:mongo@127.0.0.1:27017 33 | MONGODB_NAME=blog 34 | ``` 35 | 36 | Then, build & run: 37 | 38 | ``` Bash 39 | cargo run 40 | ``` 41 | 42 | GraphiQL: connect to http://127.0.0.1:8000/gql/v1i with browser. 43 | 44 | ![Graphql Image](../data/graphiql.jpg) 45 | 46 | ## Queries 47 | 48 | - userById(id: ObjectId!): User! 49 | - userByEmail(email: String!): User! 50 | - userByUsername(username: String!): User! 51 | - userSignIn(signature: String!, password: String!): - SignInfo! 52 | - users(token: String!): [User!]! 53 | - articles(published: Int!): [Article!]! 54 | - articlesInPosition( 55 | username: String! 56 | position: String! 57 | limit: Int! 58 | ): [Article!]! 59 | - articlesByUserId(userId: ObjectId!, published: Int!): [Article!]! 60 | - articlesByUsername(username: String!, published: Int!): [Article!]! 61 | - articlesByCategoryId(categoryId: ObjectId!, published: Int!): [Article!]! 62 | - articleBySlug(username: String!, slug: String!): Article! 63 | - categories: [Category!]! 64 | - categoriesByUserId(userId: ObjectId!): [Category!]! 65 | - categoriesByUsername(username: String!): [Category!]! 66 | - categoryById(id: ObjectId!): Category! 67 | - categoryBySlug(slug: String!): Category! 68 | - topics: [Topic!]! 69 | - topicsByArticleId(articleId: ObjectId!): [Topic!]! 70 | - wishes(published: Int!): [Wish!]! 71 | - randomWish(username: String!): Wish! 72 | 73 | ## MUTATIONS 74 | 75 | - userRegister(userNew: UserNew!): User! 76 | - userChangePassword(pwdCur: String!, pwdNew: String!, token: String!): User! 77 | - userUpdateProfile(userNew: UserNew!, token: String!): User! 78 | - articleNew(articleNew: ArticleNew!): Article! 79 | - categoryNew(categoryNew: CategoryNew!): Category! 80 | - categoryUserNew(categoryUserNew: CategoryUserNew!): CategoryUser! 81 | - topicNew(topicNew: TopicNew!): Topic! 82 | - topicArticleNew(topicArticleNew: TopicArticleNew!): TopicArticle! 83 | - wishNew(wishNew: WishNew!): Wish! 84 | 85 | ## Contributing 86 | 87 | You are welcome in contributing to this project. 88 | -------------------------------------------------------------------------------- /backend/src/articles/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod services; 3 | -------------------------------------------------------------------------------- /backend/src/articles/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use mongodb::bson::{oid::ObjectId, DateTime}; 3 | use chrono::FixedOffset; 4 | 5 | use crate::util::constant::{GqlResult, DT_F}; 6 | use crate::dbs::mongo::DataSource; 7 | use crate::categories::{self, models::Category}; 8 | use crate::topics::{self, models::Topic}; 9 | use crate::users::{self, models::User}; 10 | 11 | #[derive(Serialize, Deserialize, Clone)] 12 | pub struct Article { 13 | pub _id: ObjectId, 14 | pub user_id: ObjectId, 15 | pub subject: String, 16 | pub category_id: ObjectId, 17 | pub summary: String, 18 | pub slug: String, 19 | pub uri: String, 20 | pub content: String, 21 | pub published: bool, 22 | pub top: bool, 23 | pub recommended: bool, 24 | pub created_at: DateTime, 25 | pub updated_at: DateTime, 26 | } 27 | 28 | #[async_graphql::Object] 29 | impl Article { 30 | pub async fn id(&self) -> ObjectId { 31 | self._id.clone() 32 | } 33 | 34 | pub async fn user_id(&self) -> ObjectId { 35 | self.user_id.clone() 36 | } 37 | 38 | pub async fn subject(&self) -> &str { 39 | self.subject.as_str() 40 | } 41 | 42 | pub async fn category_id(&self) -> ObjectId { 43 | self.category_id.clone() 44 | } 45 | 46 | pub async fn summary(&self) -> &str { 47 | self.summary.as_str() 48 | } 49 | 50 | pub async fn slug(&self) -> &str { 51 | self.slug.as_str() 52 | } 53 | 54 | pub async fn uri(&self) -> &str { 55 | self.uri.as_str() 56 | } 57 | 58 | pub async fn content(&self) -> &str { 59 | self.content.as_str() 60 | } 61 | 62 | pub async fn content_html(&self) -> String { 63 | use pulldown_cmark::{Parser, Options, html}; 64 | 65 | let mut options = Options::empty(); 66 | options.insert(Options::ENABLE_TABLES); 67 | options.insert(Options::ENABLE_FOOTNOTES); 68 | options.insert(Options::ENABLE_STRIKETHROUGH); 69 | options.insert(Options::ENABLE_TASKLISTS); 70 | options.insert(Options::ENABLE_SMART_PUNCTUATION); 71 | 72 | let parser = Parser::new_ext(&self.content, options); 73 | 74 | let mut content_html = String::new(); 75 | html::push_html(&mut content_html, parser); 76 | 77 | content_html 78 | } 79 | 80 | pub async fn published(&self) -> bool { 81 | self.published 82 | } 83 | 84 | pub async fn top(&self) -> bool { 85 | self.top 86 | } 87 | 88 | pub async fn recommended(&self) -> bool { 89 | self.recommended 90 | } 91 | 92 | pub async fn created_at(&self) -> String { 93 | self.created_at 94 | .to_chrono() 95 | .with_timezone(&FixedOffset::east(8 * 3600)) 96 | .format(DT_F) 97 | .to_string() 98 | } 99 | 100 | pub async fn updated_at(&self) -> String { 101 | self.updated_at 102 | .to_chrono() 103 | .with_timezone(&FixedOffset::east(8 * 3600)) 104 | .format(DT_F) 105 | .to_string() 106 | } 107 | 108 | pub async fn user( 109 | &self, 110 | ctx: &async_graphql::Context<'_>, 111 | ) -> GqlResult { 112 | let db = ctx.data_unchecked::().db.clone(); 113 | users::services::user_by_id(db, self.user_id).await 114 | } 115 | 116 | pub async fn category( 117 | &self, 118 | ctx: &async_graphql::Context<'_>, 119 | ) -> GqlResult { 120 | let db = ctx.data_unchecked::().db.clone(); 121 | categories::services::category_by_id(db, self.category_id).await 122 | } 123 | 124 | pub async fn topics( 125 | &self, 126 | ctx: &async_graphql::Context<'_>, 127 | ) -> GqlResult> { 128 | let db = ctx.data_unchecked::().db.clone(); 129 | topics::services::topics_by_article_id(db, self._id).await 130 | } 131 | } 132 | 133 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 134 | pub struct ArticleNew { 135 | pub user_id: ObjectId, 136 | pub subject: String, 137 | pub category_id: ObjectId, 138 | pub summary: String, 139 | #[graphql(skip)] 140 | pub slug: String, 141 | #[graphql(skip)] 142 | pub uri: String, 143 | pub content: String, 144 | #[graphql(skip)] 145 | pub published: bool, 146 | #[graphql(skip)] 147 | pub top: bool, 148 | #[graphql(skip)] 149 | pub recommended: bool, 150 | } 151 | -------------------------------------------------------------------------------- /backend/src/categories/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod services; 3 | -------------------------------------------------------------------------------- /backend/src/categories/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use mongodb::bson::{oid::ObjectId, DateTime}; 3 | use chrono::FixedOffset; 4 | 5 | use crate::util::constant::{GqlResult, DT_F}; 6 | use crate::dbs::mongo::DataSource; 7 | use crate::{ 8 | articles::{models::Article, services::articles_by_category_id}, 9 | topics::{models::Topic, services::topics_by_category_id}, 10 | }; 11 | 12 | #[derive(Serialize, Deserialize, Clone)] 13 | pub struct Category { 14 | pub _id: ObjectId, 15 | pub name: String, 16 | pub description: String, 17 | pub quotes: i64, 18 | pub slug: String, 19 | pub uri: String, 20 | pub created_at: DateTime, 21 | pub updated_at: DateTime, 22 | } 23 | 24 | #[async_graphql::Object] 25 | impl Category { 26 | pub async fn id(&self) -> ObjectId { 27 | self._id.clone() 28 | } 29 | 30 | pub async fn name(&self) -> &str { 31 | self.name.as_str() 32 | } 33 | 34 | pub async fn description(&self) -> &str { 35 | self.description.as_str() 36 | } 37 | 38 | pub async fn quotes(&self) -> i64 { 39 | self.quotes 40 | } 41 | 42 | pub async fn slug(&self) -> &str { 43 | self.slug.as_str() 44 | } 45 | 46 | pub async fn uri(&self) -> &str { 47 | self.uri.as_str() 48 | } 49 | 50 | pub async fn created_at(&self) -> String { 51 | self.created_at 52 | .to_chrono() 53 | .with_timezone(&FixedOffset::east(8 * 3600)) 54 | .format(DT_F) 55 | .to_string() 56 | } 57 | 58 | pub async fn updated_at(&self) -> String { 59 | self.updated_at 60 | .to_chrono() 61 | .with_timezone(&FixedOffset::east(8 * 3600)) 62 | .format(DT_F) 63 | .to_string() 64 | } 65 | 66 | pub async fn articles( 67 | &self, 68 | ctx: &async_graphql::Context<'_>, 69 | ) -> GqlResult> { 70 | let db = ctx.data_unchecked::().db.clone(); 71 | articles_by_category_id(db, self._id, 1).await 72 | } 73 | 74 | pub async fn topics( 75 | &self, 76 | ctx: &async_graphql::Context<'_>, 77 | ) -> GqlResult> { 78 | let db = ctx.data_unchecked::().db.clone(); 79 | topics_by_category_id(db, self._id, 1).await 80 | } 81 | } 82 | 83 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 84 | pub struct CategoryNew { 85 | pub name: String, 86 | pub description: String, 87 | #[graphql(skip)] 88 | pub quotes: i64, 89 | #[graphql(skip)] 90 | pub slug: String, 91 | #[graphql(skip)] 92 | pub uri: String, 93 | } 94 | 95 | #[derive(Serialize, Deserialize, Clone)] 96 | pub struct CategoryUser { 97 | pub _id: ObjectId, 98 | pub user_id: ObjectId, 99 | pub category_id: ObjectId, 100 | } 101 | 102 | #[async_graphql::Object] 103 | impl CategoryUser { 104 | pub async fn id(&self) -> ObjectId { 105 | self._id.clone() 106 | } 107 | 108 | pub async fn user_id(&self) -> ObjectId { 109 | self.user_id.clone() 110 | } 111 | 112 | pub async fn category_id(&self) -> ObjectId { 113 | self.category_id.clone() 114 | } 115 | } 116 | 117 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 118 | pub struct CategoryUserNew { 119 | pub user_id: ObjectId, 120 | pub category_id: ObjectId, 121 | } 122 | -------------------------------------------------------------------------------- /backend/src/dbs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mongo; 2 | // pub mod postgres; 3 | // pub mod mysql; 4 | -------------------------------------------------------------------------------- /backend/src/dbs/mongo.rs: -------------------------------------------------------------------------------- 1 | use crate::util::constant::CFG; 2 | 3 | use mongodb::{Client, options::ClientOptions, Database}; 4 | 5 | pub struct DataSource { 6 | client: Client, 7 | pub db: Database, 8 | } 9 | 10 | #[allow(dead_code)] 11 | impl DataSource { 12 | pub async fn client(&self) -> Client { 13 | self.client.clone() 14 | } 15 | 16 | pub async fn init() -> DataSource { 17 | // Parse a connection string into an options struct. 18 | // environment variables defined in .env file 19 | let mut client_options = 20 | ClientOptions::parse(CFG.get("MONGODB_URI").unwrap()) 21 | .await 22 | .expect("Failed to parse options!"); 23 | // Manually set an option. 24 | client_options.app_name = Some("surfer".to_string()); 25 | 26 | // Get a handle to the deployment. 27 | let client = Client::with_options(client_options) 28 | .expect("Failed to initialize database!"); 29 | 30 | // Get a handle to a database. 31 | let db = client.database(CFG.get("MONGODB_NAME").unwrap()); 32 | 33 | // return mongodb datasource. 34 | DataSource { client: client, db } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/gql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod queries; 2 | pub mod mutations; 3 | 4 | use tide::{http::mime, Request, Response, StatusCode, Body}; 5 | 6 | use async_graphql::{ 7 | Schema, EmptySubscription, 8 | http::{playground_source, GraphQLPlaygroundConfig, receive_json}, 9 | }; 10 | 11 | use crate::State; 12 | 13 | use crate::util::constant::CFG; 14 | use crate::dbs::mongo; 15 | 16 | use crate::gql::queries::QueryRoot; 17 | use crate::gql::mutations::MutationRoot; 18 | 19 | pub async fn build_schema() -> Schema 20 | { 21 | // get mongodb datasource. It can be added to: 22 | // 1. As global data for async-graphql. 23 | // 2. As application scope state of Tide 24 | // 3. Use lazy-static.rs. 25 | let mongo_ds = mongo::DataSource::init().await; 26 | 27 | // The root object for the query and Mutatio, and use EmptySubscription. 28 | // Add global mongodb datasource in the schema object. 29 | // let mut schema = Schema::new(QueryRoot, MutationRoot, EmptySubscription) 30 | Schema::build(QueryRoot, MutationRoot, EmptySubscription) 31 | .data(mongo_ds) 32 | .finish() 33 | } 34 | 35 | pub async fn graphql(req: Request) -> tide::Result { 36 | let schema = req.state().schema.clone(); 37 | let gql_resp = schema.execute(receive_json(req).await?).await; 38 | 39 | let mut resp = Response::new(StatusCode::Ok); 40 | resp.set_body(Body::from_json(&gql_resp)?); 41 | 42 | Ok(resp.into()) 43 | } 44 | 45 | pub async fn graphiql(_: Request) -> tide::Result { 46 | let mut resp = Response::new(StatusCode::Ok); 47 | resp.set_body(playground_source(GraphQLPlaygroundConfig::new( 48 | CFG.get("GQL_VER").unwrap(), 49 | ))); 50 | resp.set_content_type(mime::HTML); 51 | 52 | Ok(resp.into()) 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/gql/mutations.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::Context; 2 | 3 | use crate::dbs::mongo::DataSource; 4 | use crate::util::constant::GqlResult; 5 | use crate::users::{ 6 | self, 7 | models::{User, UserNew, Wish, WishNew}, 8 | }; 9 | use crate::articles::{ 10 | self, 11 | models::{Article, ArticleNew}, 12 | }; 13 | use crate::categories::{ 14 | self, 15 | models::{Category, CategoryNew, CategoryUser, CategoryUserNew}, 16 | }; 17 | use crate::topics::{ 18 | self, 19 | models::{Topic, TopicNew, TopicArticle, TopicArticleNew}, 20 | }; 21 | 22 | pub struct MutationRoot; 23 | 24 | #[async_graphql::Object] 25 | impl MutationRoot { 26 | // Add new user 27 | async fn user_register( 28 | &self, 29 | ctx: &Context<'_>, 30 | user_new: UserNew, 31 | ) -> GqlResult { 32 | let db = ctx.data_unchecked::().db.clone(); 33 | users::services::user_register(db, user_new).await 34 | } 35 | 36 | // Change user password 37 | async fn user_change_password( 38 | &self, 39 | ctx: &Context<'_>, 40 | pwd_cur: String, 41 | pwd_new: String, 42 | token: String, 43 | ) -> GqlResult { 44 | let db = ctx.data_unchecked::().db.clone(); 45 | users::services::user_change_password(db, &pwd_cur, &pwd_new, &token) 46 | .await 47 | } 48 | 49 | // update user profile 50 | async fn user_update_profile( 51 | &self, 52 | ctx: &Context<'_>, 53 | user_new: UserNew, 54 | token: String, 55 | ) -> GqlResult { 56 | let db = ctx.data_unchecked::().db.clone(); 57 | users::services::user_update_profile(db, user_new, &token).await 58 | } 59 | 60 | // Add new article 61 | async fn article_new( 62 | &self, 63 | ctx: &Context<'_>, 64 | article_new: ArticleNew, 65 | ) -> GqlResult
{ 66 | let db = ctx.data_unchecked::().db.clone(); 67 | articles::services::article_new(db, article_new).await 68 | } 69 | 70 | // Add new category 71 | async fn category_new( 72 | &self, 73 | ctx: &Context<'_>, 74 | category_new: CategoryNew, 75 | ) -> GqlResult { 76 | let db = ctx.data_unchecked::().db.clone(); 77 | categories::services::category_new(db, category_new).await 78 | } 79 | 80 | // Add new category 81 | async fn category_user_new( 82 | &self, 83 | ctx: &Context<'_>, 84 | category_user_new: CategoryUserNew, 85 | ) -> GqlResult { 86 | let db = ctx.data_unchecked::().db.clone(); 87 | categories::services::category_user_new(db, category_user_new).await 88 | } 89 | 90 | // Add new topics 91 | async fn topics_new( 92 | &self, 93 | ctx: &Context<'_>, 94 | topic_names: String, 95 | ) -> GqlResult> { 96 | let db = ctx.data_unchecked::().db.clone(); 97 | topics::services::topics_new(db, &topic_names).await 98 | } 99 | 100 | // Add new topic 101 | async fn topic_new( 102 | &self, 103 | ctx: &Context<'_>, 104 | topic_new: TopicNew, 105 | ) -> GqlResult { 106 | let db = ctx.data_unchecked::().db.clone(); 107 | topics::services::topic_new(db, topic_new).await 108 | } 109 | 110 | // Add new topic_article 111 | async fn topic_article_new( 112 | &self, 113 | ctx: &Context<'_>, 114 | topic_article_new: TopicArticleNew, 115 | ) -> GqlResult { 116 | let db = ctx.data_unchecked::().db.clone(); 117 | topics::services::topic_article_new(db, topic_article_new).await 118 | } 119 | 120 | // Add new wish 121 | async fn wish_new( 122 | &self, 123 | ctx: &Context<'_>, 124 | wish_new: WishNew, 125 | ) -> GqlResult { 126 | let db = ctx.data_unchecked::().db.clone(); 127 | users::services::wish_new(db, wish_new).await 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /backend/src/main.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | mod dbs; 3 | mod gql; 4 | 5 | mod users; 6 | mod articles; 7 | mod categories; 8 | mod topics; 9 | 10 | use tide::http::headers::HeaderValue; 11 | use tide::security::{CorsMiddleware, Origin}; 12 | 13 | use crate::util::constant::CFG; 14 | use crate::gql::{build_schema, graphql, graphiql}; 15 | 16 | #[async_std::main] 17 | async fn main() -> Result<(), std::io::Error> { 18 | // tide logger 19 | tide::log::start(); 20 | 21 | // Initialize the application with state. 22 | let schema = build_schema().await; 23 | let app_state = State { schema: schema }; 24 | let mut app = tide::with_state(app_state); 25 | 26 | //environment variables defined in .env file 27 | let mut gql = app.at(CFG.get("GQL_URI").unwrap()); 28 | gql.at(CFG.get("GQL_VER").unwrap()).post(graphql); 29 | gql.at(CFG.get("GIQL_VER").unwrap()).get(graphiql); 30 | 31 | let cors = CorsMiddleware::new() 32 | .allow_methods("GET, POST, OPTIONS".parse::().unwrap()) 33 | .allow_origin(Origin::from("*")) 34 | .allow_credentials(false); 35 | app.with(cors); 36 | 37 | app.listen(format!( 38 | "{}:{}", 39 | CFG.get("ADDR").unwrap(), 40 | CFG.get("PORT").unwrap() 41 | )) 42 | .await?; 43 | 44 | Ok(()) 45 | } 46 | 47 | // Tide application scope state. 48 | #[derive(Clone)] 49 | pub struct State { 50 | pub schema: async_graphql::Schema< 51 | gql::queries::QueryRoot, 52 | gql::mutations::MutationRoot, 53 | async_graphql::EmptySubscription, 54 | >, 55 | } 56 | -------------------------------------------------------------------------------- /backend/src/topics/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod services; 3 | -------------------------------------------------------------------------------- /backend/src/topics/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use mongodb::bson::{oid::ObjectId, DateTime}; 3 | use chrono::FixedOffset; 4 | 5 | use crate::util::constant::{GqlResult, DT_F}; 6 | use crate::dbs::mongo::DataSource; 7 | use crate::articles::{models::Article, services::articles_by_topic_id}; 8 | 9 | #[derive(Serialize, Deserialize, Clone)] 10 | pub struct Topic { 11 | pub _id: ObjectId, 12 | pub name: String, 13 | pub quotes: i64, 14 | pub slug: String, 15 | pub uri: String, 16 | pub created_at: DateTime, 17 | pub updated_at: DateTime, 18 | } 19 | 20 | #[async_graphql::Object] 21 | impl Topic { 22 | pub async fn id(&self) -> ObjectId { 23 | self._id.clone() 24 | } 25 | 26 | pub async fn name(&self) -> &str { 27 | self.name.as_str() 28 | } 29 | 30 | pub async fn quotes(&self) -> i64 { 31 | self.quotes 32 | } 33 | 34 | pub async fn slug(&self) -> &str { 35 | self.slug.as_str() 36 | } 37 | 38 | pub async fn uri(&self) -> &str { 39 | self.uri.as_str() 40 | } 41 | 42 | pub async fn created_at(&self) -> String { 43 | self.created_at 44 | .to_chrono() 45 | .with_timezone(&FixedOffset::east(8 * 3600)) 46 | .format(DT_F) 47 | .to_string() 48 | } 49 | 50 | pub async fn updated_at(&self) -> String { 51 | self.updated_at 52 | .to_chrono() 53 | .with_timezone(&FixedOffset::east(8 * 3600)) 54 | .format(DT_F) 55 | .to_string() 56 | } 57 | 58 | pub async fn articles( 59 | &self, 60 | ctx: &async_graphql::Context<'_>, 61 | ) -> GqlResult> { 62 | let db = ctx.data_unchecked::().db.clone(); 63 | articles_by_topic_id(db, self._id, 1).await 64 | } 65 | } 66 | 67 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 68 | pub struct TopicNew { 69 | pub name: String, 70 | #[graphql(skip)] 71 | pub quotes: i64, 72 | #[graphql(skip)] 73 | pub slug: String, 74 | #[graphql(skip)] 75 | pub uri: String, 76 | } 77 | 78 | #[derive(Serialize, Deserialize, Clone)] 79 | pub struct TopicArticle { 80 | pub _id: ObjectId, 81 | pub user_id: ObjectId, 82 | pub article_id: ObjectId, 83 | pub topic_id: ObjectId, 84 | } 85 | 86 | #[async_graphql::Object] 87 | impl TopicArticle { 88 | pub async fn id(&self) -> ObjectId { 89 | self._id.clone() 90 | } 91 | 92 | pub async fn user_id(&self) -> ObjectId { 93 | self.user_id.clone() 94 | } 95 | 96 | pub async fn article_id(&self) -> ObjectId { 97 | self.article_id.clone() 98 | } 99 | 100 | pub async fn topic_id(&self) -> ObjectId { 101 | self.topic_id.clone() 102 | } 103 | } 104 | 105 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 106 | pub struct TopicArticleNew { 107 | pub user_id: ObjectId, 108 | pub article_id: ObjectId, 109 | pub topic_id: ObjectId, 110 | } 111 | -------------------------------------------------------------------------------- /backend/src/users/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod services; 3 | -------------------------------------------------------------------------------- /backend/src/users/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use mongodb::bson::{oid::ObjectId, DateTime}; 3 | use chrono::FixedOffset; 4 | 5 | use crate::util::constant::{GqlResult, DT_F}; 6 | use crate::dbs::mongo::DataSource; 7 | use crate::articles::{models::Article, services::articles_by_user_id}; 8 | 9 | #[derive(Serialize, Deserialize, Clone)] 10 | pub struct User { 11 | pub _id: ObjectId, 12 | pub email: String, 13 | pub username: String, 14 | pub nickname: String, 15 | pub picture: String, 16 | pub cred: String, 17 | pub blog_name: String, 18 | pub website: String, 19 | pub introduction: String, 20 | pub created_at: DateTime, 21 | pub updated_at: DateTime, 22 | pub banned: bool, 23 | } 24 | 25 | #[async_graphql::Object] 26 | impl User { 27 | pub async fn id(&self) -> ObjectId { 28 | self._id.clone() 29 | } 30 | 31 | pub async fn email(&self) -> &str { 32 | self.email.as_str() 33 | } 34 | 35 | pub async fn username(&self) -> &str { 36 | self.username.as_str() 37 | } 38 | 39 | pub async fn nickname(&self) -> &str { 40 | self.nickname.as_str() 41 | } 42 | 43 | pub async fn picture(&self) -> &str { 44 | self.picture.as_str() 45 | } 46 | 47 | pub async fn blog_name(&self) -> &str { 48 | self.blog_name.as_str() 49 | } 50 | 51 | pub async fn website(&self) -> &str { 52 | self.website.as_str() 53 | } 54 | 55 | pub async fn introduction(&self) -> &str { 56 | self.introduction.as_str() 57 | } 58 | 59 | pub async fn created_at(&self) -> String { 60 | self.created_at 61 | .to_chrono() 62 | .with_timezone(&FixedOffset::east(8 * 3600)) 63 | .format(DT_F) 64 | .to_string() 65 | } 66 | 67 | pub async fn updated_at(&self) -> String { 68 | self.updated_at 69 | .to_chrono() 70 | .with_timezone(&FixedOffset::east(8 * 3600)) 71 | .format(DT_F) 72 | .to_string() 73 | } 74 | 75 | pub async fn banned(&self) -> bool { 76 | self.banned 77 | } 78 | 79 | pub async fn articles( 80 | &self, 81 | ctx: &async_graphql::Context<'_>, 82 | published: i32, 83 | ) -> GqlResult> { 84 | let db = ctx.data_unchecked::().db.clone(); 85 | articles_by_user_id(db, self._id, published).await 86 | } 87 | } 88 | 89 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 90 | pub struct UserNew { 91 | pub email: String, 92 | pub username: String, 93 | pub nickname: String, 94 | pub picture: String, 95 | pub cred: String, 96 | pub blog_name: String, 97 | pub website: String, 98 | pub introduction: String, 99 | #[graphql(skip)] 100 | pub banned: bool, 101 | } 102 | 103 | #[derive(Serialize, Deserialize, Clone)] 104 | pub struct SignInfo { 105 | pub email: String, 106 | pub username: String, 107 | pub token: String, 108 | } 109 | 110 | #[async_graphql::Object] 111 | impl SignInfo { 112 | pub async fn email(&self) -> &str { 113 | self.email.as_str() 114 | } 115 | 116 | pub async fn username(&self) -> &str { 117 | self.username.as_str() 118 | } 119 | 120 | pub async fn token(&self) -> &str { 121 | self.token.as_str() 122 | } 123 | } 124 | 125 | #[derive(Serialize, Deserialize, Clone)] 126 | pub struct Wish { 127 | pub _id: ObjectId, 128 | pub user_id: ObjectId, 129 | pub aphorism: String, 130 | pub author: String, 131 | pub published: bool, 132 | pub created_at: DateTime, 133 | pub updated_at: DateTime, 134 | } 135 | 136 | #[async_graphql::Object] 137 | impl Wish { 138 | pub async fn id(&self) -> ObjectId { 139 | self._id.clone() 140 | } 141 | 142 | pub async fn user_id(&self) -> ObjectId { 143 | self.user_id.clone() 144 | } 145 | 146 | pub async fn aphorism(&self) -> &str { 147 | self.aphorism.as_str() 148 | } 149 | 150 | pub async fn author(&self) -> &str { 151 | self.author.as_str() 152 | } 153 | 154 | pub async fn published(&self) -> bool { 155 | self.published 156 | } 157 | 158 | pub async fn created_at(&self) -> String { 159 | self.created_at 160 | .to_chrono() 161 | .with_timezone(&FixedOffset::east(8 * 3600)) 162 | .format(DT_F) 163 | .to_string() 164 | } 165 | 166 | pub async fn updated_at(&self) -> String { 167 | self.updated_at 168 | .to_chrono() 169 | .with_timezone(&FixedOffset::east(8 * 3600)) 170 | .format(DT_F) 171 | .to_string() 172 | } 173 | 174 | pub async fn user( 175 | &self, 176 | ctx: &async_graphql::Context<'_>, 177 | ) -> GqlResult { 178 | let db = ctx.data_unchecked::().db.clone(); 179 | super::services::user_by_id(db, self.user_id).await 180 | } 181 | } 182 | 183 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 184 | pub struct WishNew { 185 | pub user_id: ObjectId, 186 | pub aphorism: String, 187 | pub author: String, 188 | #[graphql(skip)] 189 | pub published: bool, 190 | } 191 | -------------------------------------------------------------------------------- /backend/src/util/common.rs: -------------------------------------------------------------------------------- 1 | // Generate friendly slug from the given string 2 | pub async fn slugify(str: &str) -> String { 3 | use deunicode::deunicode_with_tofu; 4 | 5 | let slug = deunicode_with_tofu(str.trim(), "-") 6 | .to_lowercase() 7 | .replace(" ", "-") 8 | .replace("[", "-") 9 | .replace("]", "-") 10 | .replace("\"", "-") 11 | .replace("/", "-") 12 | .replace("?", "-"); 13 | 14 | slug 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/util/constant.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use lazy_static::lazy_static; 3 | use std::collections::HashMap; 4 | 5 | // async-graphql result type 6 | pub type GqlResult = std::result::Result; 7 | 8 | // datetime format 9 | pub const DT_F: &str = "%Y-%m-%d %H:%M:%S%Z"; 10 | 11 | lazy_static! { 12 | // CFG variables defined in .env file 13 | pub static ref CFG: HashMap<&'static str, String> = { 14 | dotenv().ok(); 15 | 16 | let mut map = HashMap::new(); 17 | 18 | map.insert( 19 | "ADDR", 20 | dotenv::var("ADDR").expect("Expected ADDR to be set in env!"), 21 | ); 22 | map.insert( 23 | "PORT", 24 | dotenv::var("PORT").expect("Expected PORT to be set in env!"), 25 | ); 26 | 27 | map.insert( 28 | "SITE_KEY", 29 | dotenv::var("SITE_KEY").expect("Expected SITE_KEY to be set in env!"), 30 | ); 31 | map.insert( 32 | "CLAIM_EXP", 33 | dotenv::var("CLAIM_EXP").expect("Expected CLAIM_EXP to be set in env!"), 34 | ); 35 | 36 | map.insert( 37 | "GQL_URI", 38 | dotenv::var("GQL_URI").expect("Expected GQL_URI to be set in env!"), 39 | ); 40 | map.insert( 41 | "GQL_VER", 42 | dotenv::var("GQL_VER").expect("Expected GQL_VER to be set in env!"), 43 | ); 44 | map.insert( 45 | "GIQL_VER", 46 | dotenv::var("GIQL_VER").expect("Expected GIQL_VER to be set in env!"), 47 | ); 48 | 49 | map.insert( 50 | "MONGODB_URI", 51 | dotenv::var("MONGODB_URI").expect("Expected MONGODB_URI to be set in env!"), 52 | ); 53 | map.insert( 54 | "MONGODB_NAME", 55 | dotenv::var("MONGODB_NAME").expect("Expected MONGODB_NAME to be set in env!"), 56 | ); 57 | 58 | map 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/util/cred.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU32; 2 | use ring::{digest, pbkdf2}; 3 | use serde::{Serialize, Deserialize}; 4 | use jsonwebtoken::{ 5 | decode, TokenData, Algorithm, DecodingKey, Validation, errors::Error, 6 | }; 7 | 8 | use crate::util::constant::CFG; 9 | 10 | static PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256; 11 | 12 | // The salt should have a user-specific component so that an attacker 13 | // cannot crack one password for multiple users. 14 | async fn salt(username: &str) -> Vec { 15 | let salt_component: [u8; 16] = [ 16 | // This value was generated from a secure PRNG. 17 | 0xd6, 0x26, 0x98, 0xda, 0xf4, 0xdc, 0x50, 0x52, 0x24, 0xf2, 0x27, 0xd1, 18 | 0xfe, 0x39, 0x01, 0x8a, 19 | ]; 20 | 21 | let mut salt = 22 | Vec::with_capacity(salt_component.len() + username.as_bytes().len()); 23 | 24 | salt.extend(salt_component.as_ref()); 25 | salt.extend(username.as_bytes()); 26 | 27 | salt 28 | } 29 | 30 | pub async fn cred_encode(username: &str, password: &str) -> String { 31 | const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN; 32 | type Credential = [u8; CREDENTIAL_LEN]; 33 | 34 | let salt = salt(username).await; 35 | 36 | let mut cred: Credential = [0u8; CREDENTIAL_LEN]; 37 | pbkdf2::derive( 38 | PBKDF2_ALG, 39 | NonZeroU32::new(100_000).unwrap(), 40 | &salt, 41 | password.as_bytes(), 42 | &mut cred, 43 | ); 44 | 45 | base64::encode(&cred) 46 | } 47 | 48 | pub async fn cred_verify( 49 | username: &str, 50 | pwd_try: &str, 51 | actual_cred: &str, 52 | ) -> bool { 53 | let salt = salt(username).await; 54 | let actual_cred_decode = base64::decode(actual_cred.as_bytes()).unwrap(); 55 | 56 | pbkdf2::verify( 57 | PBKDF2_ALG, 58 | NonZeroU32::new(100_000).unwrap(), 59 | &salt, 60 | pwd_try.as_bytes(), 61 | &actual_cred_decode, 62 | ) 63 | .is_ok() 64 | } 65 | 66 | #[derive(Debug, Serialize, Deserialize)] 67 | pub struct Claims { 68 | pub email: String, 69 | pub username: String, 70 | pub exp: usize, 71 | } 72 | 73 | pub async fn token_data(token: &str) -> Result, Error> { 74 | let site_key = CFG.get("SITE_KEY").unwrap().as_bytes(); 75 | 76 | let data = decode::( 77 | token, 78 | &DecodingKey::from_secret(site_key), 79 | &Validation::new(Algorithm::HS512), 80 | ); 81 | 82 | data 83 | } 84 | -------------------------------------------------------------------------------- /backend/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cred; 2 | pub mod constant; 3 | pub mod common; 4 | -------------------------------------------------------------------------------- /data/graphiql.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/data/graphiql.jpg -------------------------------------------------------------------------------- /data/handlebars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/data/handlebars.png -------------------------------------------------------------------------------- /data/yew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/data/yew.png -------------------------------------------------------------------------------- /frontend-handlebars/.env.example: -------------------------------------------------------------------------------- 1 | ADDR=127.0.0.1 2 | PORT=3000 3 | 4 | GQL_PROT=http 5 | GQL_ADDR=127.0.0.1 6 | GQL_PORT=8000 7 | GQL_URI=gql 8 | GQL_VER=v1 9 | GIQL_VER=v1i 10 | -------------------------------------------------------------------------------- /frontend-handlebars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surfer-frontend-handlebars" 3 | version = "0.0.1" 4 | authors = ["zzy <9809920@qq.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | async-std = { path = "../../../crates/async-std", features = ["attributes"] } 9 | tide = { path = "../../../crates/tide", features = ["logger", "cookies"] } 10 | 11 | dotenv = "0.15" 12 | lazy_static = "1.4" 13 | 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | 17 | surf = "2.3" 18 | graphql_client = "0.10" 19 | handlebars = { version = "4.2", features = ["script_helper"] } 20 | 21 | [dev-dependencies] 22 | rhai = "1.7" 23 | -------------------------------------------------------------------------------- /frontend-handlebars/README.md: -------------------------------------------------------------------------------- 1 | # Web Application Server - handlebars 2 | 3 | Demo site: [https://blog.ruonou.com](https://blog.ruonou.com) 4 | 5 | ## Build & run 6 | 7 | ``` Bash 8 | git clone https://github.com/zzy/surfer.git 9 | cd surfer 10 | cargo build 11 | 12 | cd frontend-handlebars 13 | ``` 14 | 15 | Rename file `.env.example` to `.env`, or put the environment variables into a `.env` file: 16 | 17 | ``` 18 | ADDR=127.0.0.1 19 | PORT=3000 20 | 21 | GQL_PROT=http 22 | GQL_ADDR=127.0.0.1 23 | GQL_PORT=8000 24 | GQL_URI=gql 25 | GQL_VER=v1 26 | GIQL_VER=v1i 27 | ``` 28 | 29 | Build & Run: 30 | 31 | ``` Bash 32 | cargo run 33 | ``` 34 | Then connect to http://127.0.0.1:3000 with browser. 35 | 36 | ![Client Image](../data/handlebars.png) 37 | 38 | See also: https://github.com/zzy/tide-async-graphql-mongodb/tree/main/frontend-handlebars 39 | 40 | ## How to Test & Run `rhai scripts` 41 | 42 | You could use `rhai-repl` to test your rhai code, and use `rhai-run` to run it. `rhai-repl.rs` and `rhai-run.rs` are in the folder `frontend-handlebars/scripts`, please copy them into `frontend-handlebars/examples` folder, then test or run rhai code with command: 43 | 44 | ``` bash 45 | cargo run --example / 46 | ``` 47 | 48 | If you would want to install the rhai tool, use the command 49 | 50 | ``` bash 51 | cargo install --path . --example / 52 | ``` 53 | 54 | then test rhai code using `rhai-repl`, and run scripts using the `rhai-run`: 55 | 56 | ``` bash 57 | rhai-run ./scripts/script_to_run.rhai 58 | ``` 59 | 60 | ## Contributing 61 | 62 | You are welcome in contributing to this project. 63 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/article_index.graphql: -------------------------------------------------------------------------------- 1 | query ArticleIndexData( 2 | $username: String!, 3 | $slug: String! 4 | ) { 5 | categoriesByUsername( 6 | username: $username 7 | ) { 8 | name 9 | description 10 | uri 11 | } 12 | 13 | articleBySlug( 14 | username: $username, 15 | slug: $slug 16 | ) { 17 | subject 18 | summary 19 | slug 20 | contentHtml 21 | updatedAt 22 | 23 | user { 24 | username 25 | nickname 26 | blogName 27 | } 28 | 29 | category { 30 | name 31 | uri 32 | } 33 | 34 | topics { 35 | name 36 | uri 37 | } 38 | } 39 | 40 | randomWish( 41 | username: $username 42 | ) { 43 | aphorism 44 | author 45 | 46 | user { 47 | username 48 | nickname 49 | blogName 50 | } 51 | } 52 | 53 | topicsByUsername( 54 | username: $username 55 | ) { 56 | name 57 | quotes 58 | uri 59 | } 60 | 61 | articlesByUsername( 62 | username: $username 63 | published: 1 64 | ) { 65 | subject 66 | slug 67 | 68 | category { 69 | name 70 | uri 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/article_new.graphql: -------------------------------------------------------------------------------- 1 | mutation ArticleNewData( 2 | $userId: ObjectId! 3 | $subject: String! 4 | $categoryId: ObjectId! 5 | $summary: String! 6 | $content: String! 7 | ) { 8 | articleNew( 9 | articleNew: { 10 | userId: $userId 11 | subject: $subject 12 | categoryId: $categoryId 13 | summary: $summary 14 | content: $content 15 | } 16 | ) { 17 | id 18 | slug 19 | uri 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/articles_list.graphql: -------------------------------------------------------------------------------- 1 | query ArticlesList { 2 | articles { 3 | id 4 | userId 5 | subject 6 | slug 7 | uri 8 | content 9 | createdAt 10 | updatedAt 11 | published 12 | top 13 | recommended 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/index.graphql: -------------------------------------------------------------------------------- 1 | query IndexData( 2 | $signIn: Boolean! 3 | $username: String! 4 | ) { 5 | userByUsername( 6 | username: $username 7 | ) 8 | @include(if: $signIn) { 9 | username 10 | nickname 11 | blogName 12 | picture 13 | } 14 | 15 | categories { 16 | name 17 | description 18 | uri 19 | } 20 | 21 | topArticles: articlesInPosition( 22 | username: "-" 23 | position: "top" 24 | limit: 1 25 | ) { 26 | subject 27 | category { 28 | name 29 | uri 30 | } 31 | summary 32 | slug 33 | updatedAt 34 | topics { 35 | name 36 | uri 37 | } 38 | user { 39 | username 40 | nickname 41 | blogName 42 | } 43 | } 44 | 45 | recommendedArticles: articlesInPosition( 46 | username: "-" 47 | position: "recommended" 48 | limit: 2 49 | ) { 50 | subject 51 | category { 52 | name 53 | uri 54 | } 55 | summary 56 | slug 57 | updatedAt 58 | topics { 59 | name 60 | uri 61 | } 62 | user { 63 | username 64 | nickname 65 | picture 66 | } 67 | } 68 | 69 | randomWish( 70 | username: "-" 71 | ) { 72 | aphorism 73 | author 74 | 75 | user { 76 | username 77 | nickname 78 | blogName 79 | } 80 | } 81 | 82 | articles( 83 | published: 1 84 | ) { 85 | subject 86 | category { 87 | name 88 | uri 89 | } 90 | summary 91 | slug 92 | updatedAt 93 | topics { 94 | name 95 | uri 96 | } 97 | user { 98 | username 99 | nickname 100 | } 101 | } 102 | 103 | topics { 104 | name 105 | quotes 106 | uri 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/register.graphql: -------------------------------------------------------------------------------- 1 | mutation RegisterData( 2 | $email: String! 3 | $username: String! 4 | $nickname: String! 5 | $picture: String! 6 | $cred: String! 7 | $blogName: String! 8 | $website: String! 9 | $introduction: String! 10 | ) { 11 | userRegister( 12 | userNew: { 13 | email: $email 14 | username: $username 15 | nickname: $nickname 16 | picture: $picture 17 | cred: $cred 18 | blogName: $blogName 19 | website: $website 20 | introduction: $introduction 21 | } 22 | ) { 23 | email 24 | username 25 | nickname 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | mutation: MutationRoot 4 | } 5 | 6 | # Directs the executor to query only when the field exists. 7 | directive @ifdef on FIELD 8 | 9 | type Article { 10 | id: ObjectId! 11 | userId: ObjectId! 12 | subject: String! 13 | categoryId: ObjectId! 14 | summary: String! 15 | slug: String! 16 | uri: String! 17 | content: String! 18 | contentHtml: String! 19 | published: Boolean! 20 | top: Boolean! 21 | recommended: Boolean! 22 | createdAt: String! 23 | updatedAt: String! 24 | user: User! 25 | category: Category! 26 | topics: [Topic!]! 27 | } 28 | 29 | input ArticleNew { 30 | userId: ObjectId! 31 | subject: String! 32 | categoryId: ObjectId! 33 | summary: String! 34 | content: String! 35 | } 36 | 37 | type Category { 38 | id: ObjectId! 39 | name: String! 40 | description: String! 41 | quotes: Int! 42 | slug: String! 43 | uri: String! 44 | createdAt: String! 45 | updatedAt: String! 46 | articles: [Article!]! 47 | topics: [Topic!]! 48 | } 49 | 50 | input CategoryNew { 51 | name: String! 52 | description: String! 53 | } 54 | 55 | type CategoryUser { 56 | id: ObjectId! 57 | userId: ObjectId! 58 | categoryId: ObjectId! 59 | } 60 | 61 | input CategoryUserNew { 62 | userId: ObjectId! 63 | categoryId: ObjectId! 64 | } 65 | 66 | type MutationRoot { 67 | userRegister(userNew: UserNew!): User! 68 | userChangePassword(pwdCur: String!, pwdNew: String!, token: String!): User! 69 | userUpdateProfile(userNew: UserNew!, token: String!): User! 70 | articleNew(articleNew: ArticleNew!): Article! 71 | categoryNew(categoryNew: CategoryNew!): Category! 72 | categoryUserNew(categoryUserNew: CategoryUserNew!): CategoryUser! 73 | topicsNew(topicNames: String!): [Topic!]! 74 | topicNew(topicNew: TopicNew!): Topic! 75 | topicArticleNew(topicArticleNew: TopicArticleNew!): TopicArticle! 76 | wishNew(wishNew: WishNew!): Wish! 77 | } 78 | 79 | scalar ObjectId 80 | 81 | type QueryRoot { 82 | userById(id: ObjectId!): User! 83 | userByEmail(email: String!): User! 84 | userByUsername(username: String!): User! 85 | userSignIn(signature: String!, password: String!): SignInfo! 86 | users(token: String!): [User!]! 87 | articleBySlug(username: String!, slug: String!): Article! 88 | articles(published: Int!): [Article!]! 89 | articlesInPosition( 90 | username: String! 91 | position: String! 92 | limit: Int! 93 | ): [Article!]! 94 | articlesByUserId(userId: ObjectId!, published: Int!): [Article!]! 95 | articlesByUsername(username: String!, published: Int!): [Article!]! 96 | articlesByCategoryId(categoryId: ObjectId!, published: Int!): [Article!]! 97 | articlesByTopicId(topicId: ObjectId!, published: Int!): [Article!]! 98 | categories: [Category!]! 99 | categoriesByUserId(userId: ObjectId!): [Category!]! 100 | categoriesByUsername(username: String!): [Category!]! 101 | categoryById(id: ObjectId!): Category! 102 | categoryBySlug(slug: String!): Category! 103 | topics: [Topic!]! 104 | topicById(id: ObjectId!): Topic! 105 | topicBySlug(slug: String!): Topic! 106 | topicsByArticleId(articleId: ObjectId!): [Topic!]! 107 | topicsByUserId(userId: ObjectId!): [Topic!]! 108 | topicsByUsername(username: String!): [Topic!]! 109 | topicsByCategoryId(categoryId: ObjectId!, published: Int!): [Topic!]! 110 | wishes(published: Int!): [Wish!]! 111 | randomWish(username: String!): Wish! 112 | } 113 | 114 | type SignInfo { 115 | email: String! 116 | username: String! 117 | token: String! 118 | } 119 | 120 | type Topic { 121 | id: ObjectId! 122 | name: String! 123 | quotes: Int! 124 | slug: String! 125 | uri: String! 126 | createdAt: String! 127 | updatedAt: String! 128 | articles: [Article!]! 129 | } 130 | 131 | type TopicArticle { 132 | id: ObjectId! 133 | userId: ObjectId! 134 | articleId: ObjectId! 135 | topicId: ObjectId! 136 | } 137 | 138 | input TopicArticleNew { 139 | userId: ObjectId! 140 | articleId: ObjectId! 141 | topicId: ObjectId! 142 | } 143 | 144 | input TopicNew { 145 | name: String! 146 | } 147 | 148 | type User { 149 | id: ObjectId! 150 | email: String! 151 | username: String! 152 | nickname: String! 153 | picture: String! 154 | blogName: String! 155 | website: String! 156 | introduction: String! 157 | createdAt: String! 158 | updatedAt: String! 159 | banned: Boolean! 160 | articles(published: Int!): [Article!]! 161 | } 162 | 163 | input UserNew { 164 | email: String! 165 | username: String! 166 | nickname: String! 167 | picture: String! 168 | cred: String! 169 | blogName: String! 170 | website: String! 171 | introduction: String! 172 | } 173 | 174 | type Wish { 175 | id: ObjectId! 176 | userId: ObjectId! 177 | aphorism: String! 178 | author: String! 179 | published: Boolean! 180 | createdAt: String! 181 | updatedAt: String! 182 | user: User! 183 | } 184 | 185 | input WishNew { 186 | userId: ObjectId! 187 | aphorism: String! 188 | author: String! 189 | } 190 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/sign_in.graphql: -------------------------------------------------------------------------------- 1 | query SignInData( 2 | $signature: String! 3 | $password: String! 4 | ) { 5 | userSignIn( 6 | signature: $signature 7 | password: $password 8 | ) { 9 | email 10 | username 11 | token 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/topic_article_new.graphql: -------------------------------------------------------------------------------- 1 | mutation TopicArticleNewData( 2 | $userId: ObjectId! 3 | $articleId: ObjectId! 4 | $topicId: ObjectId! 5 | ) { 6 | topicArticleNew( 7 | topicArticleNew: { 8 | userId: $userId 9 | articleId: $articleId 10 | topicId: $topicId 11 | } 12 | ) { 13 | id 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/topics_new.graphql: -------------------------------------------------------------------------------- 1 | mutation TopicsNewData( 2 | $topicNames: String! 3 | ) { 4 | topicsNew( 5 | topicNames: $topicNames 6 | ) { 7 | id 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/user_dashboard.graphql: -------------------------------------------------------------------------------- 1 | query UserDashboardData( 2 | $signIn: Boolean! 3 | $username: String! 4 | ) { 5 | userByUsername( 6 | username: $username 7 | ) 8 | @include(if: $signIn) { 9 | id 10 | username 11 | nickname 12 | blogName 13 | picture 14 | } 15 | 16 | categories { 17 | id 18 | name 19 | description 20 | uri 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/user_index.graphql: -------------------------------------------------------------------------------- 1 | query UserIndexData( 2 | $username: String! 3 | ) { 4 | userByUsername( 5 | username: $username 6 | ) { 7 | username 8 | nickname 9 | blogName 10 | website 11 | introduction 12 | } 13 | 14 | categoriesByUsername( 15 | username: $username 16 | ) { 17 | name 18 | description 19 | uri 20 | } 21 | 22 | topArticles: articlesInPosition( 23 | username: $username 24 | position: "top" 25 | limit: 1 26 | ) { 27 | subject 28 | category { 29 | name 30 | uri 31 | } 32 | summary 33 | slug 34 | updatedAt 35 | topics { 36 | name 37 | uri 38 | } 39 | user { 40 | username 41 | nickname 42 | blogName 43 | } 44 | } 45 | 46 | recommendedArticles: articlesInPosition( 47 | username: $username 48 | position: "recommended" 49 | limit: 2 50 | ) { 51 | subject 52 | category { 53 | name 54 | uri 55 | } 56 | summary 57 | slug 58 | updatedAt 59 | topics { 60 | name 61 | uri 62 | } 63 | user { 64 | username 65 | nickname 66 | picture 67 | } 68 | } 69 | 70 | randomWish( 71 | username: $username 72 | ) { 73 | aphorism 74 | author 75 | 76 | user { 77 | username 78 | nickname 79 | blogName 80 | } 81 | } 82 | 83 | articlesByUsername( 84 | username: $username 85 | published: 1 86 | ) { 87 | subject 88 | category { 89 | name 90 | uri 91 | } 92 | summary 93 | slug 94 | updatedAt 95 | topics { 96 | name 97 | uri 98 | } 99 | user { 100 | username 101 | nickname 102 | } 103 | } 104 | 105 | topicsByUsername( 106 | username: $username 107 | ) { 108 | name 109 | quotes 110 | uri 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/user_info.graphql: -------------------------------------------------------------------------------- 1 | query UserInfoData($username: String!) { 2 | userByUsername(username: $username) { 3 | username 4 | nickname 5 | blogName 6 | website 7 | introduction 8 | picture 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/users_list.graphql: -------------------------------------------------------------------------------- 1 | query UsersList( 2 | $token: String! 3 | ) { 4 | users( 5 | token: $token 6 | ) { 7 | id 8 | email 9 | username 10 | nickname 11 | blogName 12 | website 13 | banned 14 | createdAt 15 | updatedAt 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/rhai-run.rs: -------------------------------------------------------------------------------- 1 | use rhai::{Engine, EvalAltResult, Position}; 2 | 3 | #[cfg(not(feature = "no_optimize"))] 4 | use rhai::OptimizationLevel; 5 | 6 | use std::{env, fs::File, io::Read, path::Path, process::exit}; 7 | 8 | fn eprint_error(input: &str, mut err: EvalAltResult) { 9 | fn eprint_line(lines: &[&str], pos: Position, err_msg: &str) { 10 | let line = pos.line().unwrap(); 11 | let line_no = format!("{}: ", line); 12 | 13 | eprintln!("{}{}", line_no, lines[line - 1]); 14 | eprintln!( 15 | "{:>1$} {2}", 16 | "^", 17 | line_no.len() + pos.position().unwrap(), 18 | err_msg 19 | ); 20 | eprintln!(""); 21 | } 22 | 23 | let lines: Vec<_> = input.split('\n').collect(); 24 | 25 | // Print error 26 | let pos = err.take_position(); 27 | 28 | if pos.is_none() { 29 | // No position 30 | eprintln!("{}", err); 31 | } else { 32 | // Specific position 33 | eprint_line(&lines, pos, &err.to_string()) 34 | } 35 | } 36 | 37 | fn main() { 38 | let mut contents = String::new(); 39 | 40 | for filename in env::args().skip(1) { 41 | let filename = match Path::new(&filename).canonicalize() { 42 | Err(err) => { 43 | eprintln!("Error script file path: {}\n{}", filename, err); 44 | exit(1); 45 | } 46 | Ok(f) => f, 47 | }; 48 | 49 | let mut engine = Engine::new(); 50 | 51 | #[cfg(not(feature = "no_optimize"))] 52 | engine.set_optimization_level(OptimizationLevel::Full); 53 | 54 | let mut f = match File::open(&filename) { 55 | Err(err) => { 56 | eprintln!( 57 | "Error reading script file: {}\n{}", 58 | filename.to_string_lossy(), 59 | err 60 | ); 61 | exit(1); 62 | } 63 | Ok(f) => f, 64 | }; 65 | 66 | contents.clear(); 67 | 68 | if let Err(err) = f.read_to_string(&mut contents) { 69 | eprintln!( 70 | "Error reading script file: {}\n{}", 71 | filename.to_string_lossy(), 72 | err 73 | ); 74 | exit(1); 75 | } 76 | 77 | let contents = if contents.starts_with("#!") { 78 | // Skip shebang 79 | &contents[contents.find('\n').unwrap_or(0)..] 80 | } else { 81 | &contents[..] 82 | }; 83 | 84 | if let Err(err) = engine 85 | .compile(contents) 86 | .map_err(|err| Box::new(err.into()) as Box) 87 | .and_then(|mut ast| { 88 | ast.set_source(filename.to_string_lossy().to_string()); 89 | engine.consume_ast(&ast) 90 | }) 91 | { 92 | let filename = filename.to_string_lossy(); 93 | 94 | eprintln!("{:=<1$}", "", filename.len()); 95 | eprintln!("{}", filename); 96 | eprintln!("{:=<1$}", "", filename.len()); 97 | eprintln!(""); 98 | 99 | eprint_error(contents, *err); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/sci-format.rhai: -------------------------------------------------------------------------------- 1 | let number = params[0]; 2 | 3 | if number < 1000 { 4 | number 5 | } else { 6 | (number.to_float() / 100.0).round() / 10.0 + "k" 7 | } 8 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/str-trc.rhai: -------------------------------------------------------------------------------- 1 | let summary = params[0]; 2 | let str_len = params[1]; 3 | 4 | summary.sub_string(0, str_len) 5 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/value-check.rhai: -------------------------------------------------------------------------------- 1 | let exist_value = params[0]; 2 | let default_value = params[1]; 3 | 4 | if exist_value != () { 5 | exist_value 6 | } else { 7 | default_value 8 | } 9 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/website-svg.rhai: -------------------------------------------------------------------------------- 1 | let website = params[0]; 2 | 3 | let website_svg = "M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5z"; 4 | let github_svg = "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"; 5 | let facebook_svg = "M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"; 6 | let twitter_svg = "M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"; 7 | 8 | if website == () { 9 | github_svg 10 | } else { 11 | if website.contains("github.com") { 12 | github_svg 13 | } else if website.contains("facebook.com") { 14 | facebook_svg 15 | } else if website.contains("twitter.com") { 16 | twitter_svg 17 | } else { 18 | website_svg 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend-handlebars/src/main.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | mod routes; 3 | mod models; 4 | 5 | use crate::util::constant::CFG; 6 | 7 | #[async_std::main] 8 | async fn main() -> Result<(), std::io::Error> { 9 | // tide logger 10 | tide::log::start(); 11 | 12 | // Initialize the application with state. 13 | // Something in Tide State 14 | let app_state = State {}; 15 | let mut app = tide::with_state(app_state); 16 | // app = push_res(app).await; 17 | routes::push_res(&mut app).await; 18 | 19 | app.listen(format!( 20 | "{}:{}", 21 | CFG.get("ADDR").unwrap(), 22 | CFG.get("PORT").unwrap() 23 | )) 24 | .await?; 25 | 26 | Ok(()) 27 | } 28 | 29 | // Tide application scope state. 30 | #[derive(Clone)] 31 | pub struct State {} 32 | -------------------------------------------------------------------------------- /frontend-handlebars/src/models/articles.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct ArticleInfo { 5 | pub user_id: String, 6 | pub subject: String, 7 | pub category_id: String, 8 | pub summary: String, 9 | pub topic_names: String, 10 | pub content: String, 11 | } 12 | -------------------------------------------------------------------------------- /frontend-handlebars/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod users; 2 | pub mod articles; 3 | -------------------------------------------------------------------------------- /frontend-handlebars/src/models/users.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct SignInInfo { 5 | pub signature: String, 6 | pub password: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | pub struct RegisterInfo { 11 | pub email: String, 12 | pub username: String, 13 | pub nickname: String, 14 | pub password: String, 15 | pub blog_name: String, 16 | pub website: String, 17 | pub introduction: String, 18 | } 19 | 20 | #[derive(Deserialize)] 21 | pub struct ArticleInfo { 22 | pub content: String, 23 | } 24 | -------------------------------------------------------------------------------- /frontend-handlebars/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | use tide::{self, Server}; 2 | 3 | pub mod home; 4 | pub mod users; 5 | pub mod articles; 6 | 7 | use crate::State; 8 | use crate::util::common::tpls_dir; 9 | 10 | use crate::routes::home::{index, register, sign_in, sign_out}; 11 | use crate::routes::users::{user_index, user_dashboard, users_list}; 12 | use crate::routes::articles::{article_index, articles_list, article_new}; 13 | 14 | pub async fn push_res(app: &mut Server) { 15 | app.at("/").get(index); 16 | app.at("/static/*").serve_dir("./static/").unwrap(); 17 | app.at("/ads.txt") 18 | .serve_file(format!("{}{}", tpls_dir().await, "ads.txt")) 19 | .unwrap(); 20 | 21 | let mut home = app.at("/"); 22 | home.at("/register").get(register).post(register); 23 | home.at("/sign-in").get(sign_in).post(sign_in); 24 | home.at("/sign-out").get(sign_out); 25 | 26 | let mut user = app.at("/:username"); 27 | user.at("/").get(user_index); 28 | user.at("/dashboard").get(user_dashboard); 29 | user.at("/:slug").get(article_index); 30 | 31 | let mut users = app.at("/users"); 32 | users.at("/").get(users_list); 33 | 34 | let mut articles = app.at("/articles"); 35 | articles.at("/").get(articles_list); 36 | articles.at("/new").get(article_new).post(article_new); 37 | } 38 | -------------------------------------------------------------------------------- /frontend-handlebars/src/routes/users.rs: -------------------------------------------------------------------------------- 1 | use tide::{Request, Response, Redirect}; 2 | use std::collections::BTreeMap; 3 | use graphql_client::{GraphQLQuery, Response as GqlResponse}; 4 | use serde_json::json; 5 | 6 | use crate::State; 7 | use crate::util::common::{gql_uri, Tpl, get_username_from_cookies}; 8 | 9 | type ObjectId = String; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/user_index.graphql" 15 | )] 16 | struct UserIndexData; 17 | 18 | pub async fn user_index(req: Request) -> tide::Result { 19 | let username = req.param("username").unwrap(); 20 | 21 | // make data and render it 22 | let build_query = UserIndexData::build_query(user_index_data::Variables { 23 | username: username.to_string(), 24 | }); 25 | let query = json!(build_query); 26 | 27 | let resp_body: GqlResponse = 28 | surf::post(&gql_uri().await).body(query).recv_json().await.unwrap(); 29 | let resp_data = resp_body.data.expect("missing response data"); 30 | 31 | let mut user_index_tpl: Tpl = Tpl::new("users/index").await; 32 | let mut data: BTreeMap<&str, serde_json::Value> = BTreeMap::new(); 33 | 34 | if get_username_from_cookies(req).is_some() { 35 | let user = resp_data["userByUsername"].clone(); 36 | data.insert("user", user); 37 | } 38 | 39 | let categories = resp_data["categoriesByUsername"].clone(); 40 | data.insert("categories", categories); 41 | 42 | let top_articles = resp_data["topArticles"].clone(); 43 | data.insert("top_articles", top_articles); 44 | 45 | let recommended_articles = resp_data["recommendedArticles"].clone(); 46 | data.insert("recommended_articles", recommended_articles); 47 | 48 | let wish = resp_data["randomWish"].clone(); 49 | data.insert("wish", wish); 50 | 51 | let articles = resp_data["articlesByUsername"].clone(); 52 | data.insert("articles", articles); 53 | 54 | let topics = resp_data["topicsByUsername"].clone(); 55 | data.insert("topics", topics); 56 | 57 | user_index_tpl.reg_head(&mut data).await; 58 | user_index_tpl.reg_header(&mut data).await; 59 | user_index_tpl.reg_nav(&mut data).await; 60 | user_index_tpl.reg_introduction(&mut data).await; 61 | user_index_tpl.reg_topic(&mut data).await; 62 | user_index_tpl.reg_elsewhere(&mut data).await; 63 | user_index_tpl.reg_pagination(&mut data).await; 64 | user_index_tpl.reg_footer(&mut data).await; 65 | 66 | user_index_tpl.reg_script_value_check().await; 67 | user_index_tpl.reg_script_website_svg().await; 68 | user_index_tpl.reg_script_sci_format().await; 69 | user_index_tpl.reg_script_str_trc().await; 70 | 71 | user_index_tpl.render(&data).await 72 | } 73 | 74 | #[derive(GraphQLQuery)] 75 | #[graphql( 76 | schema_path = "./graphql/schema.graphql", 77 | query_path = "./graphql/user_dashboard.graphql", 78 | response_derives = "Debug" 79 | )] 80 | struct UserDashboardData; 81 | 82 | pub async fn user_dashboard(req: Request) -> tide::Result { 83 | let mut username = String::new(); 84 | if let Some(cookie) = req.cookie("username") { 85 | username.push_str(cookie.value()); 86 | } else { 87 | username.push_str("-"); 88 | } 89 | 90 | let mut sign_in = false; 91 | if "".ne(username.trim()) && "-".ne(username.trim()) { 92 | sign_in = true; 93 | } 94 | 95 | if sign_in { 96 | let build_query = 97 | UserDashboardData::build_query(user_dashboard_data::Variables { 98 | sign_in: sign_in, 99 | username: username, 100 | }); 101 | let query = json!(build_query); 102 | 103 | let resp_body: GqlResponse = 104 | surf::post(&gql_uri().await).body(query).recv_json().await?; 105 | let resp_data = resp_body.data.expect("missing response data"); 106 | 107 | let mut user_dashboard_tpl: Tpl = Tpl::new("users/dashboard").await; 108 | let mut data: BTreeMap<&str, serde_json::Value> = BTreeMap::new(); 109 | 110 | let user = resp_data["userByUsername"].clone(); 111 | data.insert("user", user); 112 | 113 | let categories = resp_data["categories"].clone(); 114 | data.insert("categories", categories); 115 | 116 | user_dashboard_tpl.reg_head(&mut data).await; 117 | user_dashboard_tpl.reg_header(&mut data).await; 118 | user_dashboard_tpl.reg_nav(&mut data).await; 119 | user_dashboard_tpl.reg_sidebar(&mut data).await; 120 | user_dashboard_tpl.reg_footer(&mut data).await; 121 | 122 | user_dashboard_tpl.reg_script_value_check().await; 123 | user_dashboard_tpl.reg_script_website_svg().await; 124 | 125 | user_dashboard_tpl.render(&data).await 126 | } else { 127 | let resp: Response = Redirect::new("/sign-in").into(); 128 | 129 | Ok(resp.into()) 130 | } 131 | } 132 | 133 | #[derive(GraphQLQuery)] 134 | #[graphql( 135 | schema_path = "./graphql/schema.graphql", 136 | query_path = "./graphql/users_list.graphql", 137 | response_derives = "Debug" 138 | )] 139 | struct UsersList; 140 | 141 | pub async fn users_list(_req: Request) -> tide::Result { 142 | let users_list_tpl: Tpl = Tpl::new("users/list").await; 143 | 144 | let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6Im9rYTIyQGJ1ZHNob21lLmNvbSIsInVzZXJuYW1lIjoi5oiRMjJz6LCBMjRvazMyIiwiZXhwIjoxMDAwMDAwMDAwMH0.FUdYJeEL1eCfturVUoPYKaVG-m4e-Jl3YJviYg1b8O9hKw2rrH7HKZED0gDT4i5lKbI9VTfbI0Qu4Tt3apwpOw"; 145 | let build_query = UsersList::build_query(users_list::Variables { 146 | token: token.to_string(), 147 | }); 148 | let query = json!(build_query); 149 | 150 | let resp_body: GqlResponse = 151 | surf::post(&gql_uri().await).body(query).recv_json().await.unwrap(); 152 | 153 | let resp_data = resp_body.data.expect("missing response data"); 154 | 155 | users_list_tpl.render(&resp_data).await 156 | } 157 | -------------------------------------------------------------------------------- /frontend-handlebars/src/util/constant.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use lazy_static::lazy_static; 3 | use std::collections::HashMap; 4 | 5 | lazy_static! { 6 | // CFG variables defined in .env file 7 | pub static ref CFG: HashMap<&'static str, String> = { 8 | dotenv().ok(); 9 | 10 | let mut map = HashMap::new(); 11 | 12 | map.insert( 13 | "ADDR", 14 | dotenv::var("ADDR").expect("Expected ADDR to be set in env!"), 15 | ); 16 | map.insert( 17 | "PORT", 18 | dotenv::var("PORT").expect("Expected PORT to be set in env!"), 19 | ); 20 | 21 | map.insert( 22 | "GQL_PROT", 23 | dotenv::var("GQL_PROT").expect("Expected GQL_PROT to be set in env!"), 24 | ); 25 | map.insert( 26 | "GQL_ADDR", 27 | dotenv::var("GQL_ADDR").expect("Expected GQL_ADDR to be set in env!"), 28 | ); 29 | map.insert( 30 | "GQL_PORT", 31 | dotenv::var("GQL_PORT").expect("Expected GQL_PORT to be set in env!"), 32 | ); 33 | map.insert( 34 | "GQL_URI", 35 | dotenv::var("GQL_URI").expect("Expected GQL_URI to be set in env!"), 36 | ); 37 | map.insert( 38 | "GQL_VER", 39 | dotenv::var("GQL_VER").expect("Expected GQL_VER to be set in env!"), 40 | ); 41 | map.insert( 42 | "GIQL_VER", 43 | dotenv::var("GIQL_VER").expect("Expected GIQL_VER to be set in env!"), 44 | ); 45 | 46 | map 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /frontend-handlebars/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constant; 2 | pub mod common; 3 | pub mod str_trait; 4 | -------------------------------------------------------------------------------- /frontend-handlebars/src/util/str_trait.rs: -------------------------------------------------------------------------------- 1 | pub trait ToFirstUppercase { 2 | fn to_first_uppercase(self) -> String; 3 | } 4 | 5 | impl ToFirstUppercase for String { 6 | fn to_first_uppercase(self) -> String { 7 | let mut c = self.chars(); 8 | match c.next() { 9 | None => String::new(), 10 | Some(f) => f.to_uppercase().collect::() + c.as_str(), 11 | } 12 | } 13 | } 14 | 15 | impl ToFirstUppercase for &str { 16 | fn to_first_uppercase(self) -> String { 17 | let mut c = self.chars(); 18 | match c.next() { 19 | None => String::new(), 20 | Some(f) => f.to_uppercase().collect::() + c.as_str(), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend-handlebars/static/articles/README.md: -------------------------------------------------------------------------------- 1 | Illustrations for articles. 2 | -------------------------------------------------------------------------------- /frontend-handlebars/static/css/kw-t.css: -------------------------------------------------------------------------------- 1 | .keyword-tags-kit { 2 | display: flex; 3 | padding: 0; 4 | border: 0 solid darkgrey; 5 | font-size: 1.5rem; 6 | } 7 | 8 | .keyword-tags { 9 | display: flex; 10 | } 11 | 12 | button.keyword-tag { 13 | display: flex; 14 | align-items: center; 15 | padding: .1rem; 16 | background-color: skyblue; 17 | border: 1px solid greenyellow; 18 | border-radius: .3rem; 19 | box-shadow: 0 .2rem .1rem rgba(0, 0, 0, 0.1); 20 | outline: none; 21 | margin-right: 0.8rem; 22 | cursor: pointer; 23 | } 24 | 25 | .keyword { 26 | padding-left: 0.2rem; 27 | } 28 | 29 | .delete-icon { 30 | width: 1rem; 31 | height: 1rem; 32 | margin-left: .1rem; 33 | background: url("/static/imgs/x.svg") center center no-repeat; 34 | background-size: 100% 100%; 35 | } 36 | 37 | .keyword-input { 38 | margin: 0; 39 | padding: 0; 40 | border: none; 41 | outline: none; 42 | flex-grow: 1; 43 | } 44 | -------------------------------------------------------------------------------- /frontend-handlebars/static/css/night-owl.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:.5em;background:#011627;color:#d6deeb}.hljs-keyword{color:#c792ea;font-style:italic}.hljs-built_in{color:#addb67;font-style:italic}.hljs-type{color:#82aaff}.hljs-literal{color:#ff5874}.hljs-number{color:#f78c6c}.hljs-regexp{color:#5ca7e4}.hljs-string{color:#ecc48d}.hljs-subst{color:#d3423e}.hljs-symbol{color:#82aaff}.hljs-class{color:#ffcb8b}.hljs-function{color:#82aaff}.hljs-title{color:#dcdcaa;font-style:italic}.hljs-params{color:#7fdbca}.hljs-comment{color:#637777;font-style:italic}.hljs-doctag{color:#7fdbca}.hljs-meta{color:#82aaff}.hljs-meta-keyword{color:#82aaff}.hljs-meta-string{color:#ecc48d}.hljs-section{color:#82b1ff}.hljs-builtin-name,.hljs-name,.hljs-tag{color:#7fdbca}.hljs-attr{color:#7fdbca}.hljs-attribute{color:#80cbc4}.hljs-variable{color:#addb67}.hljs-bullet{color:#d9f5dd}.hljs-code{color:#80cbc4}.hljs-emphasis{color:#c792ea;font-style:italic}.hljs-strong{color:#addb67;font-weight:700}.hljs-formula{color:#c792ea}.hljs-link{color:#ff869a}.hljs-quote{color:#697098;font-style:italic}.hljs-selector-tag{color:#ff6363}.hljs-selector-id{color:#fad430}.hljs-selector-class{color:#addb67;font-style:italic}.hljs-selector-attr,.hljs-selector-pseudo{color:#c792ea;font-style:italic}.hljs-template-tag{color:#c792ea}.hljs-template-variable{color:#addb67}.hljs-addition{color:#addb67ff;font-style:italic}.hljs-deletion{color:#ef535090;font-style:italic} -------------------------------------------------------------------------------- /frontend-handlebars/static/css/sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); 3 | } 4 | 5 | .sidebar .nav-link { 6 | font-weight: 500; 7 | color: #333; 8 | } 9 | 10 | .sidebar .nav-link .feather { 11 | margin-right: 4px; 12 | color: #727272; 13 | } 14 | 15 | .sidebar .nav-link.active { 16 | color: #007bff; 17 | } 18 | 19 | .sidebar .nav-link:hover .feather, 20 | .sidebar .nav-link.active .feather { 21 | color: inherit; 22 | } 23 | 24 | .sidebar-heading { 25 | font-size: .75rem; 26 | text-transform: uppercase; 27 | } 28 | -------------------------------------------------------------------------------- /frontend-handlebars/static/css/sign.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | display: flex; 7 | align-items: center; 8 | padding-top: 40px; 9 | padding-bottom: 40px; 10 | background-color: #f5f5f5; 11 | } 12 | 13 | .form-sign { 14 | width: 100%; 15 | max-width: 450px; 16 | padding: 15px; 17 | margin: auto; 18 | } 19 | .form-sign .checkbox { 20 | font-weight: 400; 21 | } 22 | .form-sign .form-control { 23 | /* position: relative; */ 24 | box-sizing: border-box; 25 | height: auto; 26 | /* padding: 10px; */ 27 | font-size: 16px; 28 | } 29 | .form-sign .form-control:focus { 30 | z-index: 2; 31 | } 32 | -------------------------------------------------------------------------------- /frontend-handlebars/static/css/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | padding-top: 40px; 8 | padding-bottom: 20px; 9 | } 10 | 11 | blockquote { 12 | margin: 10px 5px 10px 25px; 13 | padding: 10px 10px 5px 15px; 14 | background-color:#f1f1f1; 15 | border-left: 3px solid lightblue; 16 | font-size: .95rem; 17 | } 18 | 19 | article img { 20 | display: block; 21 | margin: 0 auto; 22 | } 23 | 24 | table, 25 | thead, 26 | tr, 27 | th, 28 | td { 29 | border: 1px solid gray; 30 | margin: 10px; 31 | padding: 5px; 32 | } 33 | 34 | .blog-header { 35 | line-height: 1; 36 | /* border-bottom: 1px solid #e5e5e5; */ 37 | } 38 | 39 | .blog-header-logo { 40 | font-family: "Playfair Display", Georgia, "Times New Roman", serif; 41 | font-size: 1.05rem; 42 | } 43 | 44 | .blog-header-logo:hover { 45 | text-decoration: none; 46 | } 47 | 48 | h1, h2, h3, h4, h5, h6 { 49 | font-family: "Playfair Display", Georgia, "Times New Roman", serif/*rtl:Amiri, Georgia, "Times New Roman", serif*/; 50 | } 51 | 52 | .display-4 { 53 | font-size: 2.5rem; 54 | } 55 | @media (min-width: 768px) { 56 | .display-4 { 57 | font-size: 3rem; 58 | } 59 | } 60 | 61 | .nav-scroller { 62 | position: relative; 63 | z-index: 2; 64 | height: 2.75rem; 65 | overflow-y: hidden; 66 | } 67 | 68 | .nav-scroller .nav { 69 | display: flex; 70 | flex-wrap: nowrap; 71 | padding-bottom: 1rem; 72 | /* margin-top: -1px; */ 73 | overflow-x: auto; 74 | text-align: center; 75 | white-space: nowrap; 76 | -webkit-overflow-scrolling: touch; 77 | } 78 | 79 | .nav-scroller .nav-link { 80 | padding-top: .75rem; 81 | padding-bottom: .75rem; 82 | font-size: .875rem; 83 | } 84 | 85 | .card-img-right { 86 | height: 100%; 87 | border-radius: 0 3px 3px 0; 88 | } 89 | 90 | .flex-auto { 91 | flex: 0 0 auto; 92 | } 93 | 94 | .h-250 { height: 250px; } 95 | @media (min-width: 768px) { 96 | .h-md-250 { height: 250px; } 97 | } 98 | 99 | /* Pagination */ 100 | .blog-pagination { 101 | margin-bottom: 4rem; 102 | } 103 | .blog-pagination > .btn { 104 | border-radius: 2rem; 105 | } 106 | 107 | /* 108 | * Blog posts 109 | */ 110 | .blog-post { 111 | margin-bottom: 4rem; 112 | } 113 | .blog-post-title { 114 | margin-bottom: .25rem; 115 | font-size: 2.5rem; 116 | } 117 | .blog-post-meta { 118 | margin-bottom: 1.25rem; 119 | color: #727272; 120 | } 121 | 122 | /* 123 | * Footer 124 | */ 125 | .blog-footer { 126 | padding: 3.5rem 0; 127 | color: #727272; 128 | text-align: center; 129 | background-color: #f9f9f9; 130 | border-top: .05rem solid #e5e5e5; 131 | } 132 | .blog-footer p:last-child { 133 | margin-bottom: 0; 134 | } 135 | 136 | .no-marker{ 137 | display: inherit; 138 | } 139 | -------------------------------------------------------------------------------- /frontend-handlebars/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-handlebars/static/favicon.png -------------------------------------------------------------------------------- /frontend-handlebars/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40 | 41 | -------------------------------------------------------------------------------- /frontend-handlebars/static/imgs/rust-shijian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-handlebars/static/imgs/rust-shijian.png -------------------------------------------------------------------------------- /frontend-handlebars/static/imgs/white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-handlebars/static/imgs/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend-handlebars/static/js/kw-t.js: -------------------------------------------------------------------------------- 1 | class KeywordTags extends HTMLElement { 2 | 3 | constructor() { 4 | super() 5 | 6 | var shadow = this.shadow = this.attachShadow({ mode: 'open' }) 7 | 8 | var tagsTemplate = this.tagsTemplate = document.getElementById('keyword-tags-template') 9 | shadow.appendChild(tagsTemplate.content.cloneNode(true)) 10 | 11 | var keywordTagsContainer = this.keywordTagsContainer = shadow.querySelector(".keyword-tags") 12 | var tagTemplate = this.tagTemplate = document.getElementById('keyword-tag-template') 13 | 14 | var input = this.input = shadow.querySelector("input") 15 | input.addEventListener("keydown", (e) => { 16 | var inputValue = input.value.trim(); 17 | 18 | if (e.key === " " || e.key === "Enter" || e.key === ",") { 19 | e.preventDefault(); 20 | if (inputValue !== '') 21 | this.addTag(inputValue) 22 | input.value = '' 23 | } 24 | if (e.key === "Backspace" && inputValue === '') { 25 | this.removeLastTag() 26 | } 27 | }) 28 | } 29 | 30 | addTag(tagValue) { 31 | var tagFrag = this.tagTemplate.content.cloneNode(true) 32 | var keywordNode = tagFrag.querySelector(".keyword") 33 | keywordNode.textContent = tagValue.trim() 34 | 35 | this.keywordTagsContainer.appendChild( 36 | tagFrag 37 | ) 38 | 39 | var tagNode = this.getLastTag() 40 | tagNode.addEventListener("click", (e) => { 41 | this.removeTag(tagNode) 42 | }) 43 | 44 | this.setTopics() 45 | } 46 | 47 | removeTag(tagNode) { 48 | this.keywordTagsContainer.removeChild(tagNode) 49 | 50 | this.setTopics() 51 | } 52 | 53 | removeLastTag() { 54 | var lastTag = this.getLastTag() 55 | if (lastTag !== null) 56 | this.removeTag(lastTag) 57 | } 58 | 59 | getLastTag() { 60 | var tagNodes = this.keywordTagsContainer.querySelectorAll('.keyword-tag') 61 | return tagNodes.length ? tagNodes[tagNodes.length - 1] : null 62 | } 63 | 64 | setTopics() { 65 | var topic_names = [] 66 | 67 | var tagNodes = this.keywordTagsContainer.querySelectorAll('.keyword-tag') 68 | tagNodes.forEach((tagNode) => { 69 | topic_names.push(tagNode.textContent.trim()) 70 | }) 71 | 72 | document.getElementById("topic_names").value = topic_names.toString() 73 | } 74 | 75 | connectedCallback() { 76 | var tagValues = this.getAttribute('tag-values') 77 | tagValues = tagValues.split(',') 78 | 79 | tagValues.forEach((tagValue) => { 80 | tagValue = tagValue.trim() 81 | if (tagValue !== '') 82 | this.addTag(tagValue) 83 | }) 84 | } 85 | 86 | } 87 | 88 | customElements.define('keyword-tags', KeywordTags) 89 | -------------------------------------------------------------------------------- /frontend-handlebars/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-handlebars/static/logo.png -------------------------------------------------------------------------------- /frontend-handlebars/static/users/README.md: -------------------------------------------------------------------------------- 1 | Picture of users. 2 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-2498669832870483, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /frontend-handlebars/templates/articles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ article.subject }} - {{ value-check user.blogName "云上于天" }} - 若耨 - ruonou.com 6 | 8 | {{> head }} 9 | 10 | 11 | 12 | 13 |
14 | {{> header }} 15 | {{> nav }} 16 |
17 | 18 |
19 | 20 |
21 |
22 |
{{ wish.user.nickname }} 23 | (blog: {{ wish.user.blogName }}) 24 | shared the aphorism -- 25 |
26 |
27 | {{ wish.aphorism }} -- {{ wish.author }} 28 |
29 | 30 |
31 |

32 | [{{ article.category.name }}] 33 | {{ article.subject }} 34 |

35 | 39 |

💥 内容涉及著作权,均归属作者本人。若非作者注明,默认欢迎转载:请注明出处,及相关链接。

40 |

41 | Summary: {{ article.summary }} 42 |

43 |

Topics: 44 | {{#each article.topics as |topic| }} 45 | 46 | 47 | {{ topic.name }} 48 | 49 | 50 | {{/each }} 51 |

52 |

{{{ article.contentHtml }}}

53 | {{! These can be removed }} 54 | Rust 生态与实践 55 |
56 | 57 |
58 | 59 |
60 |
61 |

Related Articles

62 |
    63 | {{#each articles as |article|}} 64 |
  1. 65 | [{{ article.category.name }}] 66 | {{ article.subject }} 67 |
  2. 68 | {{/each }} 69 |
70 |
71 | 72 | {{> topic }} 73 | {{> elsewhere }} 74 |
75 | 76 |
77 |
78 | 79 | 80 | 81 | 82 | {{> footer }} 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/articles/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 发布文章 - {{ value-check user.blogName "若耨" }} - 若耨 - ruonou.com 6 | 7 | {{> head }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | {{> header }} 17 | {{> nav }} 18 |
19 | 20 |
21 |
22 | {{> sidebar }} 23 | 24 |
25 |

New Article

26 | 27 |
28 | 29 |
30 | 32 | 33 |
34 |
35 | 43 | 44 |
45 |
46 | 48 | 49 |
50 |
51 | 52 | 53 | 54 | 61 | 67 | 71 |
72 |
73 | 78 | 79 |
80 |
81 | 83 |
84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 107 | 108 | {{> footer }} 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/articles/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all articles 6 | 7 | 8 | 9 | 10 | 11 | 12 |

all articles

13 |
    14 | {{#each articlesList as |article|}} 15 |
  • {{article.subject}}
  • 16 | 21 | {{/each}} 22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/elsewhere.html: -------------------------------------------------------------------------------- 1 |
2 |

Elsewhere

3 |
- Open Source
4 |
    5 |
  1. github/zzy
  2. 6 |
  3. github/sansx
  4. 7 |
8 | 9 |
- Learning & Studying
10 |
    11 |
  1. Rust 学习资料 - 若耨
  2. 12 |
13 |
14 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/footer.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | {{! google }} 15 | 17 | 18 | {{! baidu }} 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/header.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 88 | 89 |
90 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/introduction.html: -------------------------------------------------------------------------------- 1 |
2 |

Introduction

3 |

{{ value-check user.introduction "纯粹 Rust 技术栈开发的博客。" }}

4 |
5 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/nav.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/pagination.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/sidebar.html: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/common/topic.html: -------------------------------------------------------------------------------- 1 |
2 |

Topics

3 | {{#each topics as |topic| }} 4 |

5 | 6 | {{ topic.name }}({{ sci-format topic.quotes }}) 7 | 8 |

9 | {{/each }} 10 |
11 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 若耨博客 - blog.ruonou.com 6 | 7 | {{> head }} 8 | 9 | 10 | 11 |
12 | {{> header }} 13 | {{> nav }} 14 |
15 | 16 |
17 |
18 | {{#each top_articles as |top_article| }} 19 |
20 |

{{ top_article.subject}}

21 |

{{ top_article.summary }}

22 |
23 | 27 |
{{ top_article.updatedAt }}
28 |
29 |
30 | {{/each}} 31 |
32 | 33 |
34 | {{#each recommended_articles as |recommended_article| }} 35 |
36 |
37 |
38 | 39 | 40 | {{ recommended_article.category.name }} 41 | 42 | 43 |

{{ str-trc recommended_article.subject 44 }}

44 |
{{ recommended_article.updatedAt }}
45 |

46 | {{ str-trc recommended_article.summary 50 }} 47 |

48 | Continue reading 50 |
51 | 52 |
53 | 54 | {{ recommended_article.user.nickname }} 57 | 58 |
59 |
60 |
61 | {{/each}} 62 |
63 | 64 |
65 |
66 |
{{ wish.user.nickname }} 67 | (blog: {{ wish.user.blogName }}) 68 | shared the aphorism -- 69 |
70 |
71 | {{ wish.aphorism }} -- {{ wish.author }} 72 |
73 | 74 | {{#each articles as |article| }} 75 | 83 | {{/each}} 84 | 85 | {{> pagination }} 86 |
87 | 88 |
89 | {{> introduction }} 90 | {{> topic }} 91 | {{> elsewhere }} 92 |
93 | 94 |
95 |
96 | 97 | {{> footer }} 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 注册博客账户 - 若耨 - ruonou.com 6 | 7 | {{> head }} 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | homepage 16 | 17 | 18 | {{#if register_result }} 19 |

20 | {{ register_result.nickname }} 21 | , Congratulations! You have just created an account successfully. 22 |

23 |

24 | Please sign in BudsHome 25 | with username({{ register_result.username }}), 26 | or email address({{ register_result.email }}). 27 |

28 | {{ else }} 29 |

Please register

30 | {{#if register_failed }} 31 |

{{ register_failed }}

32 | {{/if }} 33 | 34 |
35 |
36 | 38 | 39 |
40 |
41 | 44 | 45 |
46 |
47 | 49 | 50 |
51 |
52 | 54 | 55 |
56 |
57 | 59 | 60 |
61 |
62 | 64 | 65 |
66 |
67 | 69 | 70 |
71 | 72 |

73 | Have an account? 74 | Sign in. 75 |

76 |
77 | {{/if }} 78 | 79 | {{> footer }} 80 |
81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/sign-in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 博客用户签入 - 若耨 - ruonou.com 6 | 7 | {{> head }} 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | homepage 16 | 17 |

Please sign in

18 | {{#if sign_in_failed }} 19 |

{{ sign_in_failed }}

20 | {{/if }} 21 | 22 |
23 |
24 | 26 | 27 |
28 |
29 | 31 | 32 |
33 |
34 | 38 |
39 | 40 |

41 | New to BudsHome? 42 | Create an account. 43 |

44 |
45 | 46 | {{> footer }} 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/users/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ user.nickname }} - Dashboard - 若耨 - ruonou.com 6 | 8 | {{> head }} 9 | 10 | 11 | 12 | 13 |
14 | {{> header }} 15 | {{> nav }} 16 |
17 | 18 |
19 |
20 | {{> sidebar }} 21 | 22 |
23 |

Statistics

24 | Coming soon ... 25 |
26 |
27 |
28 | 29 | {{> footer }} 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/users/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ value-check user.blogName "云上于天" }} - 若耨 - ruonou.com 6 | 8 | {{> head }} 9 | 10 | 11 | 12 |
13 | {{> header }} 14 | {{> nav }} 15 |
16 | 17 |
18 |
19 | {{#each top_articles as |top_article| }} 20 |
21 |

{{ top_article.subject}}

22 |

{{ top_article.summary }}

23 |
24 | 28 |
{{ top_article.updatedAt }}
29 |
30 |
31 | {{/each}} 32 |
33 | 34 |
35 | {{#each recommended_articles as |recommended_article| }} 36 |
37 |
38 |
39 | 40 | 41 | {{ recommended_article.category.name }} 42 | 43 | 44 |

{{ str-trc recommended_article.subject 44 }}

45 |
{{ recommended_article.updatedAt }}
46 |

47 | {{ str-trc recommended_article.summary 50 }} 48 |

49 | Continue reading 51 |
52 | 53 |
54 | 55 | {{ recommended_article.user.nickname }} 58 | 59 |
60 |
61 |
62 | {{/each}} 63 |
64 | 65 |
66 |
67 |
{{ wish.user.nickname }} 68 | (blog: {{ wish.user.blogName }}) 69 | shared the aphorism -- 70 |
71 |
72 | {{ wish.aphorism }} -- {{ wish.author }} 73 |
74 | 75 | {{#each articles as |article| }} 76 | 84 | {{/each}} 85 | 86 | {{> pagination }} 87 |
88 | 89 |
90 | {{> introduction }} 91 | {{> topic }} 92 | {{> elsewhere }} 93 |
94 | 95 |
96 |
97 | 98 | {{> footer }} 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/users/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all users 6 | 7 | 8 | 9 | 10 | 11 | 12 |

all users

13 |
    14 | {{#each usersList as |ul|}} 15 |
  • {{ul.username}}
  • 16 |
      17 |
    • {{ ul.id }}
    • 18 |
    • {{ ul.email }}
    • 19 |
    20 | {{/each}} 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/users/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all users 6 | 7 | 8 | 9 | 10 | 11 | 12 |

all users

13 |
    14 | {{#each usersList as |ul|}} 15 |
  • {{ul.username}}
  • 16 |
      17 |
    • {{ ul.id }}
    • 18 |
    • {{ ul.email }}
    • 19 |
    20 | {{/each}} 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend-yew/.env.toml.example: -------------------------------------------------------------------------------- 1 | [site] 2 | title = "若耨 | RuoNou - blog2.ruonou.com" 3 | 4 | [gql] 5 | addr = "http://127.0.0.1:8000" # for local test 6 | path = "gql/v1" 7 | 8 | [theme_mode] 9 | title = "Toggle dark mode" 10 | svg = "M3.34 14.66A8 8 0 1014.66 3.34 8 8 0 0 0 3.34 14.66zm9.9-1.42a6 6 0 01-8.48 0l8.48-8.48a6 6 0 010 8.48z" 11 | 12 | [i18n] 13 | title = "Toggle Languages" 14 | href = "#" 15 | svg = "M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z" 16 | 17 | [github] 18 | title = "RuoNou on GitHub" 19 | href = "//github.com/zzy/surfer" 20 | svg = "M9 1a8 8 0 00-2.53 15.59c.4.07.55-.17.55-.38l-.01-1.49c-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.42 7.42 0 014 0c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48l-.01 2.2c0 .21.15.46.55.38A8.01 8.01 0 009 1z" 21 | -------------------------------------------------------------------------------- /frontend-yew/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surfer-frontend-yew" 3 | version = "0.0.1" 4 | authors = ["zzy <9809920@qq.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | wasm-bindgen = "0.2" 9 | wasm-bindgen-futures = "0.4" 10 | wee_alloc = "0.4" 11 | console_error_panic_hook = "0.1" 12 | 13 | yew = "0.19" 14 | yew-router = "0.16" 15 | web-sys = { version = "0.3", features = [ 16 | "Request", 17 | "RequestInit", 18 | "RequestMode", 19 | "Response", 20 | ] } 21 | gloo = "0.7" 22 | gloo-utils = "0.1" 23 | 24 | toml = "0.5" 25 | lazy_static = "1.4" 26 | 27 | graphql_client = "0.10" 28 | serde = { version = "1.0", features = ["derive"] } 29 | serde_json = "1.0" 30 | anyhow = "1.0" 31 | -------------------------------------------------------------------------------- /frontend-yew/README.md: -------------------------------------------------------------------------------- 1 | # Web Application Server - yew 2 | 3 | Demo site: [https://blog2.ruonou.com](https://blog2.ruonou.com) 4 | 5 | ## Build & Run: 6 | 7 | ``` Bash 8 | git clone https://github.com/zzy/surfer.git 9 | cd surfer 10 | cargo build 11 | 12 | cd frontend-yew 13 | ``` 14 | 15 | Rename file `.env.toml.example` to `.env.toml`, or put the environment variables into a `.env.toml` file: 16 | 17 | ``` toml 18 | [site] 19 | title = "" 20 | 21 | [gql] 22 | addr = "http://127.0.0.1:8000" # for local test 23 | path = "gql/v1" 24 | 25 | [theme_mode] 26 | title = "" 27 | svg = "" 28 | 29 | [i18n] 30 | title = "" 31 | href = "#" 32 | svg = "" 33 | 34 | [github] 35 | title = "" 36 | href = "//github.com/zzy/surfer" 37 | svg = "" 38 | ``` 39 | 40 | > About **GraphQL API** and **MongoDB data**, read [surfer's intro](../README.md) or [surfer/backend](../backend/README.md). 41 | 42 | And then, 43 | 44 | ``` Bash 45 | cargo install trunk wasm-bindgen-cli 46 | 47 | trunk build 48 | trunk serve --release 49 | ``` 50 | Then connect to http://127.0.0.1:3001 with browser. 51 | 52 | ![Client Image](../data/yew.png) 53 | 54 | See also: https://github.com/zzy/tide-async-graphql-mongodb/tree/main/frontend-yew 55 | 56 | ## Contributing 57 | 58 | You are welcome in contributing to this project. 59 | -------------------------------------------------------------------------------- /frontend-yew/assets/ccss/style.css: -------------------------------------------------------------------------------- 1 | .bg-checkered { 2 | background-color: var(--white); 3 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cg fill='%23e4e6e8' fill-opacity='0.4'%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/%3E%3C/g%3E%3C/svg%3E"); 4 | } 5 | 6 | article blockquote { 7 | margin: 10px 5px 10px 25px; 8 | padding: 10px 10px 5px 15px; 9 | background-color: var(--blue-050); 10 | border-left: 3px solid var(--blue-600); 11 | font-size: 13px; 12 | } 13 | 14 | article code { 15 | color: var(--red); 16 | word-wrap:break-word; 17 | font-weight: bold; 18 | } 19 | 20 | article img { 21 | margin: 3px auto; 22 | display: block; 23 | max-width: 100%; 24 | } 25 | 26 | article > section > p { 27 | text-indent: 29px; 28 | } 29 | -------------------------------------------------------------------------------- /frontend-yew/assets/css/night-owl.min.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#011627;color:#d6deeb}.hljs-keyword{color:#c792ea;font-style:italic}.hljs-built_in{color:#addb67;font-style:italic}.hljs-type{color:#82aaff}.hljs-literal{color:#ff5874}.hljs-number{color:#f78c6c}.hljs-regexp{color:#5ca7e4}.hljs-string{color:#ecc48d}.hljs-subst{color:#d3423e}.hljs-symbol{color:#82aaff}.hljs-class{color:#ffcb8b}.hljs-function{color:#82aaff}.hljs-title{color:#dcdcaa;font-style:italic}.hljs-params{color:#7fdbca}.hljs-comment{color:#637777;font-style:italic}.hljs-doctag{color:#7fdbca}.hljs-meta,.hljs-meta .hljs-keyword{color:#82aaff}.hljs-meta .hljs-string{color:#ecc48d}.hljs-section{color:#82b1ff}.hljs-attr,.hljs-name,.hljs-tag{color:#7fdbca}.hljs-attribute{color:#80cbc4}.hljs-variable{color:#addb67}.hljs-bullet{color:#d9f5dd}.hljs-code{color:#80cbc4}.hljs-emphasis{color:#c792ea;font-style:italic}.hljs-strong{color:#addb67;font-weight:700}.hljs-formula{color:#c792ea}.hljs-link{color:#ff869a}.hljs-quote{color:#697098;font-style:italic}.hljs-selector-tag{color:#ff6363}.hljs-selector-id{color:#fad430}.hljs-selector-class{color:#addb67;font-style:italic}.hljs-selector-attr,.hljs-selector-pseudo{color:#c792ea;font-style:italic}.hljs-template-tag{color:#c792ea}.hljs-template-variable{color:#addb67}.hljs-addition{color:#addb67ff;font-style:italic}.hljs-deletion{color:#ef535090;font-style:italic} -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/favicons/apple-touch-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/favicons/apple-touch-icon@2x.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/favicons/favicon.ico -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/logos/open-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/logos/open-graph.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/logos/rusthub-w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/logos/rusthub-w.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/logos/rusthub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/logos/rusthub.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/rust-shijian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/surfer/8007d4c9d87a64abf5db957f938eb0a044bd72f8/frontend-yew/assets/imgs/rust-shijian.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/feature.banners.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Show the system banner when you click the "Show Example" button 3 | var topnav = $(".js-stacks-topbar"); 4 | var sysBanner = $(".js-notice-banner"); 5 | var sysBannerHeight = sysBanner.outerHeight(); 6 | var sysBannerBtn = $(".js-sys-banner-show"); 7 | var sysCloseBtn = $(".js-sys-banner-remove, .js-notice-close"); 8 | var sysStyleMenu = $(".js-sys-banner-style-menu"); 9 | var sysType = $(".js-sys-banner-type"); 10 | var sysPos = $(".js-sys-banner-position"); 11 | var sysCloseIcon = $(".js-notice-close"); 12 | var typeClasses = ("s-banner__info s-banner__success s-banner__warning s-banner__danger s-banner__dark s-banner__important is-pinned"); 13 | 14 | sysBannerBtn.on("click", function(e) { 15 | var sysStyle = sysStyleMenu.find(":selected").data("class"); 16 | 17 | e.preventDefault(); 18 | e.stopPropagation(); 19 | 20 | $(this).text("Update example"); 21 | topnav.css("top",""); 22 | sysCloseBtn.removeClass("d-none"); 23 | sysBanner.show().attr("aria-hidden","false").removeClass(typeClasses).addClass(sysStyle); 24 | sysCloseIcon.removeClass("fc-white").addClass("fc-dark"); 25 | 26 | if (sysPos.is(":checked")) { 27 | topnav.removeClass("t0").css("top", sysBannerHeight + "px"); 28 | sysBanner.addClass("is-pinned"); 29 | } 30 | 31 | if (sysType.is(":checked")) { 32 | sysBanner.addClass("s-banner__important"); 33 | 34 | if (sysStyle == "s-banner__warning" || sysStyle == "s-banner__success") { 35 | sysCloseIcon.removeClass("fc-white").addClass("fc-dark"); 36 | } 37 | else { 38 | sysCloseIcon.removeClass("fc-dark").addClass("fc-white"); 39 | } 40 | } 41 | }); 42 | 43 | sysCloseBtn.on("click", function(e) { 44 | e.preventDefault(); 45 | e.stopPropagation(); 46 | 47 | topnav.addClass("t0"); 48 | sysBanner.hide().attr("aria-hidden","true").removeClass(typeClasses); 49 | 50 | sysBannerBtn.text("Show example"); 51 | sysCloseBtn.addClass("d-none"); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/feature.darkmode.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var darkModeBtn = $(".js-darkmode-btn"); 3 | var body = $("body"); 4 | 5 | darkModeBtn.click(function (e) { 6 | e.preventDefault(); 7 | e.stopPropagation(); 8 | 9 | var browserPrefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; 10 | 11 | var isForcedDarkMode = body.hasClass("theme-dark"); 12 | var isUnforcedDarkMode = browserPrefersDark && body.hasClass("theme-system"); 13 | 14 | if (browserPrefersDark) { 15 | body.toggleClass("theme-system", !isUnforcedDarkMode); 16 | body.toggleClass("theme-dark", false); 17 | } else { 18 | body.toggleClass("theme-system", true); 19 | body.toggleClass("theme-dark", !isForcedDarkMode); 20 | } 21 | 22 | localStorage.setItem("forceDarkModeOn", !(isUnforcedDarkMode || isForcedDarkMode)); 23 | 24 | return false; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/feature.notices.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var topnav = $(".js-stacks-topbar"); 3 | var topnavHeight = topnav.outerHeight(); 4 | var toast = $(".js-notice-toast"); 5 | var closeBtn = $(".js-notice-close"); 6 | 7 | $(".js-notice-toast-open").click(function(e) { 8 | var toastOffset = topnavHeight + 16 + "px"; 9 | 10 | toast.css("top", toastOffset); 11 | 12 | toast.queue(function() { 13 | $(this).attr("aria-hidden","false").dequeue(); 14 | }) 15 | .delay(3000) 16 | .queue(function(e) { 17 | $(this).attr("aria-hidden","true").dequeue(); 18 | }); 19 | }); 20 | 21 | closeBtn.click(function(e) { 22 | e.preventDefault(); 23 | e.stopPropagation(); 24 | 25 | toast.attr("aria-hidden","true"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/hamburger.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var navigation = $(".js-navigation"); 3 | var closeIcon = $(".js-hamburger-close-icon"); 4 | var hamburgerIcon = $(".js-hamburger-icon"); 5 | var hamburgerBtn = $(".js-hamburger-btn"); 6 | 7 | hamburgerBtn.click(function(e) { 8 | e.preventDefault(); 9 | e.stopPropagation(); 10 | 11 | hamburgerIcon.toggleClass("d-none"); 12 | closeIcon.toggleClass("d-none"); 13 | navigation.toggleClass("md:d-none"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/hl.js: -------------------------------------------------------------------------------- 1 | let script = document.createElement("script"); 2 | script.src = "/js/highlight.min.js?132689068675031052"; 3 | 4 | script.onload = script.onreadystatechange = function () { 5 | if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { 6 | hljs.highlightAll(); 7 | 8 | this.onload = this.onreadystatechange = null; 9 | this.parentNode.removeChild(this); 10 | } 11 | } 12 | 13 | document.body.appendChild(script); 14 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/load.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | sequential("/js/library.jquery.js", 4 | function () { 5 | random("/js/theme.js?132689068675031052"); 6 | random("/js/hamburger.js?132689068675031052"); 7 | random("/js/navigation.js?132689068675031052"); 8 | random("/js/nav.selected.js?132689068675031052"); 9 | random("/js/search.js?132689068675031052"); 10 | random("/js/stacks.min.js?132689068675031052"); 11 | random("/js/feature.darkmode.js?132689068675031052"); 12 | // These can be removed 13 | random("//static.ruonou.com/js/bdtj-bh.js"); 14 | random("//static.ruonou.com/js/bdts.js"); 15 | } 16 | ); 17 | 18 | function random(src) { 19 | let script = document.createElement("script"); 20 | script.src = src; 21 | 22 | script.onload = script.onreadystatechange = function () { 23 | if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { 24 | this.onload = this.onreadystatechange = null; 25 | this.parentNode.removeChild(this); 26 | } 27 | } 28 | 29 | document.body.appendChild(script); 30 | }; 31 | 32 | function sequential(src, success) { 33 | let script = document.createElement("script"); 34 | script.src = src; 35 | 36 | success = success || function () { }; 37 | script.onload = script.onreadystatechange = function () { 38 | if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { 39 | success(); 40 | 41 | this.onload = this.onreadystatechange = null; 42 | this.parentNode.removeChild(this); 43 | } 44 | } 45 | 46 | document.body.appendChild(script); 47 | } 48 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/nav.selected.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var isSelected = "is-selected"; 3 | 4 | var navGlobalArticles = $(".nav-global-articles"); 5 | var navGlobalCategories = $(".nav-global-categories"); 6 | var navGlobalTopics = $(".nav-global-topics"); 7 | var navGlobalExplore = $(".nav-global-explore"); 8 | var navSignSignin = $(".nav-sign-signin"); 9 | var navSignRegister = $(".nav-sign-register"); 10 | var logo = $(".js-logo"); 11 | 12 | navGlobalArticles.click(function (e) { 13 | navSelectedClean(); 14 | navGlobalArticles.toggleClass(isSelected, true); 15 | }); 16 | 17 | navGlobalCategories.click(function (e) { 18 | navSelectedClean(); 19 | navGlobalCategories.toggleClass(isSelected, true); 20 | }); 21 | 22 | navGlobalTopics.click(function (e) { 23 | navSelectedClean(); 24 | navGlobalTopics.toggleClass(isSelected, true); 25 | }); 26 | 27 | navGlobalExplore.click(function (e) { 28 | navSelectedClean(); 29 | navGlobalExplore.toggleClass(isSelected, true); 30 | }); 31 | 32 | navSignSignin.click(function (e) { 33 | navSelectedClean(); 34 | navSignSignin.toggleClass(isSelected, true); 35 | }); 36 | 37 | navSignRegister.click(function (e) { 38 | navSelectedClean(); 39 | navSignRegister.toggleClass(isSelected, true); 40 | }); 41 | 42 | logo.click(function (e) { 43 | navSelectedClean(); 44 | }); 45 | 46 | function navSelectedClean() { 47 | navGlobalArticles.toggleClass(isSelected, false); 48 | navGlobalCategories.toggleClass(isSelected, false); 49 | navGlobalTopics.toggleClass(isSelected, false); 50 | navGlobalExplore.toggleClass(isSelected, false); 51 | navSignSignin.toggleClass(isSelected, false); 52 | navSignRegister.toggleClass(isSelected, false); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/navigation.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Cache some variables 3 | var navigation = $(".js-navigation"); 4 | var closeIcon = $(".js-hamburger-close-icon"); 5 | var hamburgerIcon = $(".js-hamburger-icon"); 6 | 7 | // Disable any empty links 8 | $("a[href='#']").click(function(e) { 9 | e.preventDefault(); 10 | }); 11 | 12 | function regenerateMenu () { 13 | // Hide the navigation if we've opened it 14 | hamburgerIcon.removeClass("d-none"); 15 | closeIcon.addClass("d-none"); 16 | navigation.addClass("md:d-none"); 17 | } 18 | 19 | function killEmptyLinks() { 20 | // Kill default behavior on empty links 21 | $("a[href='#']").on("click", function(e) { 22 | e.preventDefault(); 23 | e.stopPropagation(); 24 | }); 25 | } 26 | 27 | $.when($.ready).then(function() { 28 | regenerateMenu(); 29 | killEmptyLinks(); 30 | 31 | window.history.replaceState({ 32 | 'href': window.location.href, 33 | 'title': $('head').filter('title').text(), 34 | 'nav': $(document).find('#nav').html(), 35 | 'content': $(document).find('#content').html(), 36 | }, '', window.location.href) 37 | 38 | $('#nav').on('click', 'a', function (event) { 39 | 40 | // Allow opening links in new tabs 41 | if (event.metaKey) { 42 | return 43 | } 44 | 45 | // Prevent following link 46 | event.preventDefault() 47 | 48 | // Get desired link 49 | var href = $(this).attr('href') 50 | 51 | // Make Ajax request to get the page content 52 | $.ajax({ 53 | method: 'GET', 54 | url: href, 55 | cache: false, 56 | dataType: 'html', 57 | }).done(function(html) { 58 | 59 | // Parse the HTML response 60 | var title = $(html).filter('title').text() 61 | var nav = $(html).find('#nav').html() 62 | var content = $(html).find('#content').html() 63 | 64 | // Update the page 65 | $('head title').text(title) 66 | $('#nav').html(nav) 67 | $('#content').html(content) 68 | 69 | // Scroll to the top of the page 70 | $(document).scrollTop(0) 71 | 72 | regenerateMenu(); 73 | killEmptyLinks(); 74 | 75 | // Add page load to browser history 76 | window.history.pushState({ 77 | 'href': href, 78 | 'title': title, 79 | 'nav': $(html).find('#nav').html(), 80 | 'content': $(html).find('#content').html(), 81 | }, '', href) 82 | }) 83 | }) 84 | 85 | window.onpopstate = history.onpushstate = function(e) { 86 | if(e.state){ 87 | // Update the page 88 | $('title').text(e.state.title) 89 | $('#nav').html(e.state.nav) 90 | $('#content').html(e.state.content) 91 | } 92 | } 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/search.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Show or hide search 3 | var searchBar = $(".js-stacks-search-bar"); 4 | var searchContainer = $(".js-search"); 5 | var searchCloseIcon = $(".js-search-close-icon"); 6 | var searchIcon = $(".js-search-icon"); 7 | var searchBtn = $(".js-search-btn"); 8 | var hamburgerBtn = $(".js-hamburger-btn"); 9 | var logo = $(".js-logo"); 10 | 11 | searchBtn.click(function(e) { 12 | e.preventDefault(); 13 | e.stopPropagation(); 14 | 15 | searchIcon.toggleClass("d-none"); 16 | searchCloseIcon.toggleClass("d-none"); 17 | searchContainer.toggleClass("sm:d-none"); 18 | hamburgerBtn.toggleClass("md:d-block"); 19 | logo.toggleClass("sm:d-none"); 20 | 21 | if ( searchIcon.hasClass("d-none") ) { 22 | searchBar.focus(); 23 | } 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend-yew/assets/js/theme.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var forceSetting = localStorage.getItem("forceDarkModeOn"); 3 | var browserPrefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; 4 | var darkModeEnabled = forceSetting === "true" || (!forceSetting && browserPrefersDark); 5 | 6 | if (browserPrefersDark) { 7 | document.body.classList.toggle("theme-system", darkModeEnabled); 8 | document.body.classList.toggle("theme-dark", false); 9 | } 10 | else { 11 | document.body.classList.toggle("theme-system", true); 12 | document.body.classList.toggle("theme-dark", darkModeEnabled); 13 | } 14 | }()); 15 | -------------------------------------------------------------------------------- /frontend-yew/graphql/article.graphql: -------------------------------------------------------------------------------- 1 | query ArticleData( 2 | $username: String!, 3 | $slug: String! 4 | ) { 5 | randomWish( 6 | username: $username 7 | ) { 8 | aphorism 9 | author 10 | 11 | user { 12 | username 13 | nickname 14 | blogName 15 | } 16 | } 17 | 18 | articleBySlug( 19 | username: $username, 20 | slug: $slug 21 | ) { 22 | subject 23 | summary 24 | uri 25 | contentHtml 26 | updatedAt 27 | 28 | user { 29 | username 30 | nickname 31 | blogName 32 | } 33 | 34 | category { 35 | name 36 | uri 37 | } 38 | 39 | topics { 40 | name 41 | uri 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend-yew/graphql/articles.graphql: -------------------------------------------------------------------------------- 1 | query ArticlesData { 2 | randomWish( 3 | username: "-" 4 | ) { 5 | aphorism 6 | author 7 | 8 | user { 9 | username 10 | nickname 11 | blogName 12 | } 13 | } 14 | 15 | articles( 16 | published: 1 17 | ) { 18 | subject 19 | summary 20 | slug 21 | updatedAt 22 | 23 | user { 24 | username 25 | nickname 26 | blogName 27 | } 28 | category { 29 | name 30 | uri 31 | } 32 | 33 | topics { 34 | name 35 | uri 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend-yew/graphql/author.graphql: -------------------------------------------------------------------------------- 1 | query AuthorData( 2 | $username: String!, 3 | ) { 4 | randomWish( 5 | username: "-" 6 | ) { 7 | aphorism 8 | author 9 | 10 | user { 11 | username 12 | nickname 13 | blogName 14 | } 15 | } 16 | 17 | userByUsername( 18 | username: $username 19 | ) { 20 | username 21 | nickname 22 | blogName 23 | 24 | articles( 25 | published: 1 26 | ) { 27 | subject 28 | summary 29 | uri 30 | updatedAt 31 | 32 | user { 33 | username 34 | nickname 35 | blogName 36 | } 37 | 38 | category { 39 | name 40 | uri 41 | } 42 | 43 | topics { 44 | name 45 | uri 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend-yew/graphql/categories.graphql: -------------------------------------------------------------------------------- 1 | query CategoriesData { 2 | randomWish( 3 | username: "-" 4 | ) { 5 | aphorism 6 | author 7 | 8 | user { 9 | username 10 | nickname 11 | blogName 12 | } 13 | } 14 | 15 | categories { 16 | name 17 | uri 18 | quotes 19 | 20 | topics { 21 | name 22 | uri 23 | quotes 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend-yew/graphql/category.graphql: -------------------------------------------------------------------------------- 1 | query CategoryData( 2 | $slug: String! 3 | ) { 4 | randomWish( 5 | username: "-" 6 | ) { 7 | aphorism 8 | author 9 | 10 | user { 11 | username 12 | nickname 13 | blogName 14 | } 15 | } 16 | 17 | categoryBySlug( 18 | slug: $slug 19 | ) { 20 | name 21 | quotes 22 | 23 | articles { 24 | subject 25 | summary 26 | uri 27 | updatedAt 28 | 29 | user { 30 | username 31 | nickname 32 | blogName 33 | } 34 | 35 | category { 36 | name 37 | uri 38 | } 39 | 40 | topics { 41 | name 42 | uri 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend-yew/graphql/home.graphql: -------------------------------------------------------------------------------- 1 | query HomeData { 2 | randomWish( 3 | username: "-" 4 | ) { 5 | aphorism 6 | author 7 | 8 | user { 9 | username 10 | nickname 11 | blogName 12 | } 13 | } 14 | 15 | topArticles: articlesInPosition( 16 | username: "-" 17 | position: "top" 18 | limit: 2 19 | ) { 20 | subject 21 | summary 22 | slug 23 | updatedAt 24 | 25 | user { 26 | username 27 | nickname 28 | blogName 29 | } 30 | 31 | category { 32 | name 33 | uri 34 | } 35 | 36 | topics { 37 | name 38 | uri 39 | } 40 | } 41 | 42 | recommendedArticles: articlesInPosition( 43 | username: "-" 44 | position: "recommended" 45 | limit: 4 46 | ) { 47 | subject 48 | summary 49 | slug 50 | updatedAt 51 | 52 | user { 53 | username 54 | nickname 55 | blogName 56 | } 57 | 58 | category { 59 | name 60 | uri 61 | } 62 | 63 | topics { 64 | name 65 | uri 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend-yew/graphql/register.graphql: -------------------------------------------------------------------------------- 1 | mutation RegisterData( 2 | $email: String! 3 | $username: String! 4 | $nickname: String! 5 | $picture: String! 6 | $cred: String! 7 | $blogName: String! 8 | $website: String! 9 | $introduction: String! 10 | ) { 11 | userRegister( 12 | userNew: { 13 | email: $email 14 | username: $username 15 | nickname: $nickname 16 | picture: $picture 17 | cred: $cred 18 | blogName: $blogName 19 | website: $website 20 | introduction: $introduction 21 | } 22 | ) { 23 | email 24 | username 25 | nickname 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend-yew/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | mutation: MutationRoot 4 | } 5 | 6 | # Directs the executor to query only when the field exists. 7 | directive @ifdef on FIELD 8 | 9 | type Article { 10 | id: ObjectId! 11 | userId: ObjectId! 12 | subject: String! 13 | categoryId: ObjectId! 14 | summary: String! 15 | slug: String! 16 | uri: String! 17 | content: String! 18 | contentHtml: String! 19 | published: Boolean! 20 | top: Boolean! 21 | recommended: Boolean! 22 | createdAt: String! 23 | updatedAt: String! 24 | user: User! 25 | category: Category! 26 | topics: [Topic!]! 27 | } 28 | 29 | input ArticleNew { 30 | userId: ObjectId! 31 | subject: String! 32 | categoryId: ObjectId! 33 | summary: String! 34 | content: String! 35 | } 36 | 37 | type Category { 38 | id: ObjectId! 39 | name: String! 40 | description: String! 41 | quotes: Int! 42 | slug: String! 43 | uri: String! 44 | createdAt: String! 45 | updatedAt: String! 46 | articles: [Article!]! 47 | topics: [Topic!]! 48 | } 49 | 50 | input CategoryNew { 51 | name: String! 52 | description: String! 53 | } 54 | 55 | type CategoryUser { 56 | id: ObjectId! 57 | userId: ObjectId! 58 | categoryId: ObjectId! 59 | } 60 | 61 | input CategoryUserNew { 62 | userId: ObjectId! 63 | categoryId: ObjectId! 64 | } 65 | 66 | type MutationRoot { 67 | userRegister(userNew: UserNew!): User! 68 | userChangePassword(pwdCur: String!, pwdNew: String!, token: String!): User! 69 | userUpdateProfile(userNew: UserNew!, token: String!): User! 70 | articleNew(articleNew: ArticleNew!): Article! 71 | categoryNew(categoryNew: CategoryNew!): Category! 72 | categoryUserNew(categoryUserNew: CategoryUserNew!): CategoryUser! 73 | topicsNew(topicNames: String!): [Topic!]! 74 | topicNew(topicNew: TopicNew!): Topic! 75 | topicArticleNew(topicArticleNew: TopicArticleNew!): TopicArticle! 76 | wishNew(wishNew: WishNew!): Wish! 77 | } 78 | 79 | scalar ObjectId 80 | 81 | type QueryRoot { 82 | userById(id: ObjectId!): User! 83 | userByEmail(email: String!): User! 84 | userByUsername(username: String!): User! 85 | userSignIn(signature: String!, password: String!): SignInfo! 86 | users(token: String!): [User!]! 87 | articleBySlug(username: String!, slug: String!): Article! 88 | articles(published: Int!): [Article!]! 89 | articlesInPosition( 90 | username: String! 91 | position: String! 92 | limit: Int! 93 | ): [Article!]! 94 | articlesByUserId(userId: ObjectId!, published: Int!): [Article!]! 95 | articlesByUsername(username: String!, published: Int!): [Article!]! 96 | articlesByCategoryId(categoryId: ObjectId!, published: Int!): [Article!]! 97 | articlesByTopicId(topicId: ObjectId!, published: Int!): [Article!]! 98 | categories: [Category!]! 99 | categoriesByUserId(userId: ObjectId!): [Category!]! 100 | categoriesByUsername(username: String!): [Category!]! 101 | categoryById(id: ObjectId!): Category! 102 | categoryBySlug(slug: String!): Category! 103 | topics: [Topic!]! 104 | topicById(id: ObjectId!): Topic! 105 | topicBySlug(slug: String!): Topic! 106 | topicsByArticleId(articleId: ObjectId!): [Topic!]! 107 | topicsByUserId(userId: ObjectId!): [Topic!]! 108 | topicsByUsername(username: String!): [Topic!]! 109 | topicsByCategoryId(categoryId: ObjectId!, published: Int!): [Topic!]! 110 | wishes(published: Int!): [Wish!]! 111 | randomWish(username: String!): Wish! 112 | } 113 | 114 | type SignInfo { 115 | email: String! 116 | username: String! 117 | token: String! 118 | } 119 | 120 | type Topic { 121 | id: ObjectId! 122 | name: String! 123 | quotes: Int! 124 | slug: String! 125 | uri: String! 126 | createdAt: String! 127 | updatedAt: String! 128 | articles: [Article!]! 129 | } 130 | 131 | type TopicArticle { 132 | id: ObjectId! 133 | userId: ObjectId! 134 | articleId: ObjectId! 135 | topicId: ObjectId! 136 | } 137 | 138 | input TopicArticleNew { 139 | userId: ObjectId! 140 | articleId: ObjectId! 141 | topicId: ObjectId! 142 | } 143 | 144 | input TopicNew { 145 | name: String! 146 | } 147 | 148 | type User { 149 | id: ObjectId! 150 | email: String! 151 | username: String! 152 | nickname: String! 153 | picture: String! 154 | blogName: String! 155 | website: String! 156 | introduction: String! 157 | createdAt: String! 158 | updatedAt: String! 159 | banned: Boolean! 160 | articles(published: Int!): [Article!]! 161 | } 162 | 163 | input UserNew { 164 | email: String! 165 | username: String! 166 | nickname: String! 167 | picture: String! 168 | cred: String! 169 | blogName: String! 170 | website: String! 171 | introduction: String! 172 | } 173 | 174 | type Wish { 175 | id: ObjectId! 176 | userId: ObjectId! 177 | aphorism: String! 178 | author: String! 179 | published: Boolean! 180 | createdAt: String! 181 | updatedAt: String! 182 | user: User! 183 | } 184 | 185 | input WishNew { 186 | userId: ObjectId! 187 | aphorism: String! 188 | author: String! 189 | } 190 | -------------------------------------------------------------------------------- /frontend-yew/graphql/sign_in.graphql: -------------------------------------------------------------------------------- 1 | query SignInData( 2 | $signature: String! 3 | $password: String! 4 | ) { 5 | userSignIn( 6 | signature: $signature 7 | password: $password 8 | ) { 9 | email 10 | username 11 | token 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend-yew/graphql/topic.graphql: -------------------------------------------------------------------------------- 1 | query TopicData( 2 | $slug: String! 3 | ) { 4 | randomWish( 5 | username: "-" 6 | ) { 7 | aphorism 8 | author 9 | 10 | user { 11 | username 12 | nickname 13 | blogName 14 | } 15 | } 16 | 17 | topicBySlug( 18 | slug: $slug 19 | ) { 20 | name 21 | quotes 22 | 23 | articles { 24 | subject 25 | summary 26 | uri 27 | updatedAt 28 | 29 | user { 30 | username 31 | nickname 32 | blogName 33 | } 34 | 35 | category { 36 | name 37 | uri 38 | } 39 | 40 | topics { 41 | name 42 | uri 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend-yew/graphql/topics.graphql: -------------------------------------------------------------------------------- 1 | query TopicsData { 2 | randomWish( 3 | username: "-" 4 | ) { 5 | aphorism 6 | author 7 | 8 | user { 9 | username 10 | nickname 11 | blogName 12 | } 13 | } 14 | 15 | topics { 16 | name 17 | uri 18 | quotes 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend-yew/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 若耨 - RuoNou - blog2.ruonou.com 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 | -------------------------------------------------------------------------------- /frontend-yew/src/components/footer.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | #[function_component(Copyright)] 4 | pub fn copyright() -> Html { 5 | html! { 6 | // Please customize your footer 7 | 21 | } 22 | } 23 | 24 | #[function_component(LoadJs)] 25 | pub fn load_js() -> Html { 26 | html! { 27 | // load scripts 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend-yew/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod nodes; 2 | pub mod header; 3 | pub mod footer; 4 | -------------------------------------------------------------------------------- /frontend-yew/src/components/nodes.rs: -------------------------------------------------------------------------------- 1 | use yew::{prelude::*, virtual_dom::VNode}; 2 | use serde_json::Value; 3 | 4 | pub fn random_wish_node(wish_val: &Value) -> VNode { 5 | html! { 6 |
7 | 8 | 10 | { wish_val["user"]["nickname"].as_str().unwrap() } 11 | { "@" } 12 | { wish_val["user"]["blogName"].as_str().unwrap() } 13 | 14 | { " shared the aphorism: " } 15 | 16 | { wish_val["aphorism"].as_str().unwrap() } 17 | { " -- " } 18 | { wish_val["author"].as_str().unwrap() } 19 |
20 | } 21 | } 22 | 23 | pub fn article_card_node(article: &Value) -> VNode { 24 | let article_topics_vec = article["topics"].as_array().unwrap(); 25 | let article_topics = article_topics_vec.iter().map(|topic| { 26 | html! { 27 | 29 | { topic["name"].as_str().unwrap() } 30 | 31 | } 32 | }); 33 | 34 | html! { 35 |
36 |

37 | 40 | { article["category"]["name"].as_str().unwrap() } 41 | 42 | 43 | { article["subject"].as_str().unwrap() } 44 | 45 |

46 |

47 | { article["updatedAt"].as_str().unwrap() } 48 | { " by " } 49 | 51 | { article["user"]["nickname"].as_str().unwrap() } 52 | { "@" } 53 | { article["user"]["blogName"].as_str().unwrap() } 54 | 55 |

56 |

57 | { "Topics:" } 58 | { for article_topics } 59 |

60 |

{ article["summary"].as_str().unwrap() }

61 |
62 | } 63 | } 64 | 65 | pub fn topic_tag_node(topic: &Value) -> VNode { 66 | let topic_quotes = topic["quotes"].as_i64().unwrap(); 67 | let tag_size = if topic_quotes >= 100 { 68 | "s-tag__lg fw-bold" 69 | } else if topic_quotes < 100 && topic_quotes >= 60 { 70 | "s-tag__md fw-bold" 71 | } else if topic_quotes < 60 && topic_quotes >= 30 { 72 | "s-tag" 73 | } else if topic_quotes < 30 && topic_quotes >= 10 { 74 | "s-tag__sm" 75 | } else { 76 | "s-tag__xs" 77 | }; 78 | 79 | html! { 80 | 82 | { topic["name"].as_str().unwrap() } 83 | { "(" } 84 | { topic_quotes } 85 | { ")" } 86 | 87 | } 88 | } 89 | 90 | pub fn page_not_found() -> VNode { 91 | html! { 92 |
93 |

94 | { "无此页面" } 95 |
96 | { "Page not found" } 97 |

98 |

99 | { "似乎不存在此页面" } 100 |
101 | { "Page page does not seem to exist" } 102 |

103 |
104 | } 105 | } 106 | -------------------------------------------------------------------------------- /frontend-yew/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | #[global_allocator] 4 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 5 | 6 | mod util; 7 | mod components; 8 | mod show; 9 | mod manage; 10 | 11 | use console_error_panic_hook::set_once as set_panic_hook; 12 | use yew::prelude::*; 13 | use yew_router::prelude::*; 14 | 15 | use crate::util::routes::{Routes, switch}; 16 | use crate::components::{header::*, footer::*}; 17 | 18 | struct App; 19 | 20 | impl Component for App { 21 | type Message = (); 22 | type Properties = (); 23 | 24 | fn create(_ctx: &Context) -> Self { 25 | Self 26 | } 27 | 28 | fn view(&self, _ctx: &Context) -> Html { 29 | html! { 30 | 31 |
32 | 33 |
34 | render={ Switch::render(switch) } /> 35 |
36 | 37 | 38 | 39 | 40 | 41 | } 42 | } 43 | } 44 | 45 | fn main() { 46 | set_panic_hook(); 47 | 48 | yew::start_app::(); 49 | } 50 | -------------------------------------------------------------------------------- /frontend-yew/src/manage/manage_index.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | pub struct ManageIndex; 4 | 5 | impl Component for ManageIndex { 6 | type Message = (); 7 | type Properties = (); 8 | 9 | fn create(_ctx: &Context) -> Self { 10 | Self 11 | } 12 | 13 | fn view(&self, _ctx: &Context) -> Html { 14 | html! { 15 | <> 16 |

{ "--- ManageIndex ---" }

17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend-yew/src/manage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod manage_index; 2 | -------------------------------------------------------------------------------- /frontend-yew/src/show/article.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, page_not_found}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/article.graphql" 15 | )] 16 | struct ArticleData; 17 | 18 | async fn query_str(username: String, article_slug: String) -> String { 19 | let build_query = ArticleData::build_query(article_data::Variables { 20 | username: username, 21 | slug: article_slug, 22 | }); 23 | let query = json!(build_query); 24 | 25 | query.to_string() 26 | } 27 | 28 | pub enum Msg { 29 | SetState(FetchState), 30 | GetData, 31 | } 32 | 33 | #[derive(Clone, Debug, Eq, PartialEq, Properties)] 34 | pub struct Props { 35 | pub username: String, 36 | pub article_slug: String, 37 | } 38 | 39 | pub struct Article { 40 | data: FetchState, 41 | } 42 | 43 | impl Component for Article { 44 | type Message = Msg; 45 | type Properties = Props; 46 | 47 | fn create(_ctx: &Context) -> Self { 48 | Self { data: FetchState::NotFetching } 49 | } 50 | 51 | fn view(&self, _ctx: &Context) -> Html { 52 | match &self.data { 53 | FetchState::NotFetching => html! { "NotFetching" }, 54 | FetchState::Fetching => html! { "Fetching" }, 55 | FetchState::Success(article_data) => view_article(article_data), 56 | FetchState::Failed(err) => html! { err }, 57 | } 58 | } 59 | 60 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 61 | if first_render { 62 | ctx.link().send_message(Msg::GetData); 63 | } 64 | } 65 | 66 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 67 | match msg { 68 | Msg::SetState(fetch_state) => { 69 | self.data = fetch_state; 70 | 71 | true 72 | } 73 | Msg::GetData => { 74 | let props = ctx.props().clone(); 75 | ctx.link().send_future(async { 76 | match fetch_gql_data( 77 | &query_str(props.username, props.article_slug).await, 78 | ) 79 | .await 80 | { 81 | Ok(data) => Msg::SetState(FetchState::Success(data)), 82 | Err(err) => Msg::SetState(FetchState::Failed(err)), 83 | } 84 | }); 85 | 86 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 87 | 88 | false 89 | } 90 | } 91 | } 92 | } 93 | 94 | fn view_article(article_data: &Value) -> Html { 95 | if article_data.is_null() { 96 | page_not_found() 97 | } else { 98 | let wish_val = &article_data["randomWish"]; 99 | let random_wish = random_wish_node(wish_val); 100 | 101 | let article = &article_data["articleBySlug"]; 102 | let subject = article["subject"].as_str().unwrap(); 103 | let document = gloo_utils::document(); 104 | document.set_title(&format!( 105 | "{} - {}", 106 | subject, 107 | CFG.get("site.title").unwrap() 108 | )); 109 | 110 | let article_topics_vec = article["topics"].as_array().unwrap(); 111 | let article_topics = article_topics_vec.iter().map(|topic| { 112 | html! { 113 | 115 | { topic["name"].as_str().unwrap() } 116 | 117 | } 118 | }); 119 | 120 | let content_html = article["contentHtml"].as_str().unwrap(); 121 | let content_html_section = 122 | gloo_utils::document().create_element("section").unwrap(); 123 | content_html_section.set_class_name("fs-body2 mt24"); 124 | content_html_section.set_inner_html(content_html); 125 | let content_html_node = Html::VRef(content_html_section.into()); 126 | 127 | html! { 128 | <> 129 | { random_wish } 130 |
131 |

132 | 135 | { article["category"]["name"].as_str().unwrap() } 136 | 137 | 138 | { subject } 139 | 140 |

141 |

142 | { article["updatedAt"].as_str().unwrap() } 143 | { " by " } 144 | 146 | { article["user"]["nickname"].as_str().unwrap() } 147 | { "@" } 148 | { article["user"]["blogName"].as_str().unwrap() } 149 | 150 |

151 |

152 | { "Topics:" } 153 | { for article_topics } 154 |

155 |

156 | { "💥" } 157 | { "内容涉及著作权,均归属作者本人。" } 158 | { "若非作者注明,默认欢迎转载:请注明出处,及相关链接。" } 159 |

160 |

161 | { "Summary:" } 162 | { article["summary"].as_str().unwrap() } 163 |

164 | 165 | { content_html_node } 166 | 167 | { 168 |
169 | 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /frontend-yew/src/show/articles.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, article_card_node}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/articles.graphql" 15 | )] 16 | struct ArticlesData; 17 | 18 | async fn query_str() -> String { 19 | let build_query = ArticlesData::build_query(articles_data::Variables {}); 20 | let query = json!(build_query); 21 | 22 | query.to_string() 23 | } 24 | 25 | pub enum Msg { 26 | SetState(FetchState), 27 | GetData, 28 | } 29 | 30 | pub struct Articles { 31 | data: FetchState, 32 | } 33 | 34 | impl Component for Articles { 35 | type Message = Msg; 36 | type Properties = (); 37 | 38 | fn create(_ctx: &Context) -> Self { 39 | Self { data: FetchState::NotFetching } 40 | } 41 | 42 | fn view(&self, _ctx: &Context) -> Html { 43 | match &self.data { 44 | FetchState::NotFetching => html! { "NotFetching" }, 45 | FetchState::Fetching => html! { "Fetching" }, 46 | FetchState::Success(articles_data) => view_articles(articles_data), 47 | FetchState::Failed(err) => html! { err }, 48 | } 49 | } 50 | 51 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 52 | if first_render { 53 | ctx.link().send_message(Msg::GetData); 54 | } 55 | } 56 | 57 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 58 | match msg { 59 | Msg::SetState(fetch_state) => { 60 | self.data = fetch_state; 61 | 62 | true 63 | } 64 | Msg::GetData => { 65 | ctx.link().send_future(async { 66 | match fetch_gql_data(&query_str().await).await { 67 | Ok(data) => Msg::SetState(FetchState::Success(data)), 68 | Err(err) => Msg::SetState(FetchState::Failed(err)), 69 | } 70 | }); 71 | 72 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 73 | 74 | false 75 | } 76 | } 77 | } 78 | 79 | fn changed(&mut self, ctx: &Context) -> bool { 80 | ctx.link().send_message(Msg::GetData); 81 | 82 | false 83 | } 84 | } 85 | 86 | fn view_articles(articles_data: &Value) -> Html { 87 | let document = gloo_utils::document(); 88 | document.set_title(&format!( 89 | "{} - {}", 90 | "Articles", 91 | CFG.get("site.title").unwrap() 92 | )); 93 | 94 | let wish_val = &articles_data["randomWish"]; 95 | let random_wish = random_wish_node(wish_val); 96 | 97 | let articles_vec = articles_data["articles"].as_array().unwrap(); 98 | let articles = 99 | articles_vec.iter().map(|article| article_card_node(article)); 100 | 101 | html! { 102 | <> 103 | { random_wish } 104 | { for articles } 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /frontend-yew/src/show/author.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{ 10 | random_wish_node, article_card_node, page_not_found, 11 | }; 12 | 13 | #[derive(GraphQLQuery)] 14 | #[graphql( 15 | schema_path = "./graphql/schema.graphql", 16 | query_path = "./graphql/author.graphql" 17 | )] 18 | struct AuthorData; 19 | 20 | async fn query_str(username: String) -> String { 21 | let build_query = 22 | AuthorData::build_query(author_data::Variables { username: username }); 23 | let query = json!(build_query); 24 | 25 | query.to_string() 26 | } 27 | 28 | pub enum Msg { 29 | SetState(FetchState), 30 | GetData, 31 | } 32 | 33 | #[derive(Clone, Debug, Eq, PartialEq, Properties)] 34 | pub struct Props { 35 | pub username: String, 36 | } 37 | 38 | pub struct Author { 39 | data: FetchState, 40 | } 41 | 42 | impl Component for Author { 43 | type Message = Msg; 44 | type Properties = Props; 45 | 46 | fn create(_ctx: &Context) -> Self { 47 | Self { data: FetchState::NotFetching } 48 | } 49 | 50 | fn view(&self, _ctx: &Context) -> Html { 51 | match &self.data { 52 | FetchState::NotFetching => html! { "NotFetching" }, 53 | FetchState::Fetching => html! { "Fetching" }, 54 | FetchState::Success(author_data) => view_author(author_data), 55 | FetchState::Failed(err) => html! { err }, 56 | } 57 | } 58 | 59 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 60 | if first_render { 61 | ctx.link().send_message(Msg::GetData); 62 | } 63 | } 64 | 65 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 66 | match msg { 67 | Msg::SetState(fetch_state) => { 68 | self.data = fetch_state; 69 | 70 | true 71 | } 72 | Msg::GetData => { 73 | let props = ctx.props().clone(); 74 | ctx.link().send_future(async { 75 | match fetch_gql_data(&query_str(props.username).await).await 76 | { 77 | Ok(data) => Msg::SetState(FetchState::Success(data)), 78 | Err(err) => Msg::SetState(FetchState::Failed(err)), 79 | } 80 | }); 81 | 82 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 83 | 84 | false 85 | } 86 | } 87 | } 88 | } 89 | 90 | fn view_author(author_data: &Value) -> Html { 91 | if author_data.is_null() { 92 | page_not_found() 93 | } else { 94 | let wish_val = &author_data["randomWish"]; 95 | let random_wish = random_wish_node(wish_val); 96 | 97 | let user = &author_data["userByUsername"]; 98 | let username = user["username"].as_str().unwrap(); 99 | let nickname = user["nickname"].as_str().unwrap(); 100 | let blog_name = user["blogName"].as_str().unwrap(); 101 | let document = gloo_utils::document(); 102 | document.set_title(&format!( 103 | "{} ({}) - {} - {}", 104 | nickname, 105 | username, 106 | blog_name, 107 | CFG.get("site.title").unwrap() 108 | )); 109 | 110 | let articles_vec = user["articles"].as_array().unwrap(); 111 | let articles = 112 | articles_vec.iter().map(|article| article_card_node(article)); 113 | 114 | html! { 115 | <> 116 | { random_wish } 117 |
118 | 119 | { nickname } 120 | { "@" } 121 | { blog_name } 122 | 123 | { " 文章分享列表:" } 124 |
125 | { for articles } 126 | 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /frontend-yew/src/show/categories.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, topic_tag_node}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/categories.graphql" 15 | )] 16 | struct CategoriesData; 17 | 18 | async fn query_str() -> String { 19 | let build_query = 20 | CategoriesData::build_query(categories_data::Variables {}); 21 | let query = json!(build_query); 22 | 23 | query.to_string() 24 | } 25 | 26 | pub enum Msg { 27 | SetState(FetchState), 28 | GetData, 29 | } 30 | 31 | pub struct Categories { 32 | data: FetchState, 33 | } 34 | 35 | impl Component for Categories { 36 | type Message = Msg; 37 | type Properties = (); 38 | 39 | fn create(_ctx: &Context) -> Self { 40 | Self { data: FetchState::NotFetching } 41 | } 42 | 43 | fn view(&self, _ctx: &Context) -> Html { 44 | match &self.data { 45 | FetchState::NotFetching => html! { "NotFetching" }, 46 | FetchState::Fetching => html! { "Fetching" }, 47 | FetchState::Success(categories_data) => { 48 | view_categories(categories_data) 49 | } 50 | FetchState::Failed(err) => html! { err }, 51 | } 52 | } 53 | 54 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 55 | if first_render { 56 | ctx.link().send_message(Msg::GetData); 57 | } 58 | } 59 | 60 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 61 | match msg { 62 | Msg::SetState(fetch_state) => { 63 | self.data = fetch_state; 64 | 65 | true 66 | } 67 | Msg::GetData => { 68 | ctx.link().send_future(async { 69 | match fetch_gql_data(&query_str().await).await { 70 | Ok(data) => Msg::SetState(FetchState::Success(data)), 71 | Err(err) => Msg::SetState(FetchState::Failed(err)), 72 | } 73 | }); 74 | 75 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 76 | 77 | false 78 | } 79 | } 80 | } 81 | 82 | fn changed(&mut self, ctx: &Context) -> bool { 83 | ctx.link().send_message(Msg::GetData); 84 | 85 | false 86 | } 87 | } 88 | 89 | fn view_categories(categories_data: &Value) -> Html { 90 | let document = gloo_utils::document(); 91 | document.set_title(&format!( 92 | "{} - {}", 93 | "Categories", 94 | CFG.get("site.title").unwrap() 95 | )); 96 | 97 | let wish_val = &categories_data["randomWish"]; 98 | let random_wish = random_wish_node(wish_val); 99 | 100 | let categories_vec = categories_data["categories"].as_array().unwrap(); 101 | let categories = categories_vec.iter().map(|category| { 102 | let topics_vec = category["topics"].as_array().unwrap(); 103 | let topics = topics_vec.iter().map(|topic| topic_tag_node(topic)); 104 | 105 | html! { 106 | 121 | } 122 | }); 123 | 124 | html! { 125 | <> 126 | { random_wish } 127 | { for categories } 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /frontend-yew/src/show/category.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, article_card_node}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/category.graphql" 15 | )] 16 | struct CategoryData; 17 | 18 | async fn query_str(category_slug: String) -> String { 19 | let build_query = CategoryData::build_query(category_data::Variables { 20 | slug: category_slug, 21 | }); 22 | let query = json!(build_query); 23 | 24 | query.to_string() 25 | } 26 | 27 | pub enum Msg { 28 | SetState(FetchState), 29 | GetData, 30 | } 31 | 32 | #[derive(Clone, Debug, Eq, PartialEq, Properties)] 33 | pub struct Props { 34 | pub category_slug: String, 35 | } 36 | 37 | pub struct Category { 38 | data: FetchState, 39 | } 40 | 41 | impl Component for Category { 42 | type Message = Msg; 43 | type Properties = Props; 44 | 45 | fn create(_ctx: &Context) -> Self { 46 | Self { data: FetchState::NotFetching } 47 | } 48 | 49 | fn view(&self, _ctx: &Context) -> Html { 50 | match &self.data { 51 | FetchState::NotFetching => html! { "NotFetching" }, 52 | FetchState::Fetching => html! { "Fetching" }, 53 | FetchState::Success(category_data) => view_category(category_data), 54 | FetchState::Failed(err) => html! { err }, 55 | } 56 | } 57 | 58 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 59 | if first_render { 60 | ctx.link().send_message(Msg::GetData); 61 | } 62 | } 63 | 64 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 65 | match msg { 66 | Msg::SetState(fetch_state) => { 67 | self.data = fetch_state; 68 | 69 | true 70 | } 71 | Msg::GetData => { 72 | let props = ctx.props().clone(); 73 | ctx.link().send_future(async { 74 | match fetch_gql_data(&query_str(props.category_slug).await) 75 | .await 76 | { 77 | Ok(data) => Msg::SetState(FetchState::Success(data)), 78 | Err(err) => Msg::SetState(FetchState::Failed(err)), 79 | } 80 | }); 81 | 82 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 83 | 84 | false 85 | } 86 | } 87 | } 88 | } 89 | 90 | fn view_category(category_data: &Value) -> Html { 91 | let wish_val = &category_data["randomWish"]; 92 | let random_wish = random_wish_node(wish_val); 93 | 94 | let category = &category_data["categoryBySlug"]; 95 | let category_name = category["name"].as_str().unwrap(); 96 | let document = gloo_utils::document(); 97 | document.set_title(&format!( 98 | "{} - {}", 99 | category_name, 100 | CFG.get("site.title").unwrap() 101 | )); 102 | 103 | let articles_vec = category["articles"].as_array().unwrap(); 104 | let articles = 105 | articles_vec.iter().map(|article| article_card_node(article)); 106 | 107 | html! { 108 | <> 109 | { random_wish } 110 |
111 | { category_name } 112 | { " 类目中,文章共 " } 113 | { category["quotes"].as_i64().unwrap() } 114 | { " 篇:" } 115 |
116 | { for articles } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /frontend-yew/src/show/explore.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | pub struct Explore; 4 | 5 | impl Component for Explore { 6 | type Message = (); 7 | type Properties = (); 8 | 9 | fn create(_ctx: &Context) -> Self { 10 | Self 11 | } 12 | 13 | fn view(&self, _ctx: &Context) -> Html { 14 | html! { 15 |
16 | { "--- Explore, Work In Progress ---" } 17 | 23 |
24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend-yew/src/show/home.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, article_card_node}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/home.graphql" 15 | )] 16 | struct HomeData; 17 | 18 | async fn query_str() -> String { 19 | let build_query = HomeData::build_query(home_data::Variables {}); 20 | let query = json!(build_query); 21 | 22 | query.to_string() 23 | } 24 | 25 | pub enum Msg { 26 | SetState(FetchState), 27 | GetData, 28 | } 29 | 30 | pub struct Home { 31 | data: FetchState, 32 | } 33 | 34 | impl Component for Home { 35 | type Message = Msg; 36 | type Properties = (); 37 | 38 | fn create(_ctx: &Context) -> Self { 39 | Self { data: FetchState::NotFetching } 40 | } 41 | 42 | fn view(&self, _ctx: &Context) -> Html { 43 | match &self.data { 44 | FetchState::NotFetching => html! { "NotFetching" }, 45 | FetchState::Fetching => html! { "Fetching" }, 46 | FetchState::Success(home_data) => view_home(home_data), 47 | FetchState::Failed(err) => html! { err }, 48 | } 49 | } 50 | 51 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 52 | if first_render { 53 | ctx.link().send_message(Msg::GetData); 54 | } 55 | } 56 | 57 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 58 | match msg { 59 | Msg::SetState(fetch_state) => { 60 | self.data = fetch_state; 61 | 62 | true 63 | } 64 | Msg::GetData => { 65 | ctx.link().send_future(async { 66 | match fetch_gql_data(&query_str().await).await { 67 | Ok(data) => Msg::SetState(FetchState::Success(data)), 68 | Err(err) => Msg::SetState(FetchState::Failed(err)), 69 | } 70 | }); 71 | 72 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 73 | 74 | false 75 | } 76 | } 77 | } 78 | 79 | fn changed(&mut self, ctx: &Context) -> bool { 80 | ctx.link().send_message(Msg::GetData); 81 | 82 | false 83 | } 84 | } 85 | 86 | fn view_home(home_data: &Value) -> Html { 87 | let document = gloo_utils::document(); 88 | document.set_title(&format!( 89 | "{} - {}", 90 | "Home", 91 | CFG.get("site.title").unwrap() 92 | )); 93 | 94 | let wish_val = &home_data["randomWish"]; 95 | let random_wish = random_wish_node(wish_val); 96 | 97 | let top_articles_vec = home_data["topArticles"].as_array().unwrap(); 98 | let top_articles = top_articles_vec.iter().map(|top_article| { 99 | let article_topics_vec = top_article["topics"].as_array().unwrap(); 100 | let article_topics = article_topics_vec.iter().map(|topic| { 101 | html! { 102 | 104 | { topic["name"].as_str().unwrap() } 105 | 106 | } 107 | }); 108 | 109 | html! { 110 |
111 |

112 | 115 | { top_article["category"]["name"].as_str().unwrap() } 116 | 117 | 118 | { top_article["subject"].as_str().unwrap() } 119 | 120 |

121 |

122 | { top_article["updatedAt"].as_str().unwrap() } 123 | { " by " } 124 | 126 | { top_article["user"]["nickname"].as_str().unwrap() } 127 | { "@" } 128 | { top_article["user"]["blogName"].as_str().unwrap() } 129 | 130 |

131 |

132 | { "Topics:" } 133 | { for article_topics } 134 |

135 |

{ top_article["summary"].as_str().unwrap() }

136 |
137 | } 138 | }); 139 | 140 | let recommended_articles_vec = 141 | home_data["recommendedArticles"].as_array().unwrap(); 142 | let recommended_articles = recommended_articles_vec 143 | .iter() 144 | .map(|recommended_article| article_card_node(recommended_article)); 145 | 146 | html! { 147 | <> 148 | { random_wish } 149 |
150 | { for top_articles } 151 |
152 | { for recommended_articles } 153 | 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /frontend-yew/src/show/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod home; 2 | pub mod sign_in; 3 | pub mod register; 4 | pub mod author; 5 | pub mod articles; 6 | pub mod categories; 7 | pub mod topics; 8 | pub mod article; 9 | pub mod category; 10 | pub mod topic; 11 | pub mod explore; 12 | -------------------------------------------------------------------------------- /frontend-yew/src/show/register.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | pub struct Register; 4 | 5 | impl Component for Register { 6 | type Message = (); 7 | type Properties = (); 8 | 9 | fn create(_ctx: &Context) -> Self { 10 | Self 11 | } 12 | 13 | fn view(&self, _ctx: &Context) -> Html { 14 | html! { 15 |
16 | { "--- Register, Work In Progress ---" } 17 |
18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend-yew/src/show/sign_in.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | pub struct SignIn; 4 | 5 | impl Component for SignIn { 6 | type Message = (); 7 | type Properties = (); 8 | 9 | fn create(_ctx: &Context) -> Self { 10 | Self 11 | } 12 | fn view(&self, _ctx: &Context) -> Html { 13 | html! { 14 |
15 | { "--- Sign in, Work In Progress ---" } 16 |
17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend-yew/src/show/topic.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, article_card_node}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/topic.graphql" 15 | )] 16 | struct TopicData; 17 | 18 | async fn query_str(topic_slug: String) -> String { 19 | let build_query = 20 | TopicData::build_query(topic_data::Variables { slug: topic_slug }); 21 | let query = json!(build_query); 22 | 23 | query.to_string() 24 | } 25 | 26 | pub enum Msg { 27 | SetState(FetchState), 28 | GetData, 29 | } 30 | 31 | #[derive(Clone, Debug, Eq, PartialEq, Properties)] 32 | pub struct Props { 33 | pub topic_slug: String, 34 | } 35 | 36 | pub struct Topic { 37 | data: FetchState, 38 | } 39 | 40 | impl Component for Topic { 41 | type Message = Msg; 42 | type Properties = Props; 43 | 44 | fn create(_ctx: &Context) -> Self { 45 | Self { data: FetchState::NotFetching } 46 | } 47 | 48 | fn view(&self, _ctx: &Context) -> Html { 49 | match &self.data { 50 | FetchState::NotFetching => html! { "NotFetching" }, 51 | FetchState::Fetching => html! { "Fetching" }, 52 | FetchState::Success(topic_data) => view_topic(topic_data), 53 | FetchState::Failed(err) => html! { err }, 54 | } 55 | } 56 | 57 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 58 | if first_render { 59 | ctx.link().send_message(Msg::GetData); 60 | } 61 | } 62 | 63 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 64 | match msg { 65 | Msg::SetState(fetch_state) => { 66 | self.data = fetch_state; 67 | 68 | true 69 | } 70 | Msg::GetData => { 71 | let props = ctx.props().clone(); 72 | ctx.link().send_future(async { 73 | match fetch_gql_data(&query_str(props.topic_slug).await) 74 | .await 75 | { 76 | Ok(data) => Msg::SetState(FetchState::Success(data)), 77 | Err(err) => Msg::SetState(FetchState::Failed(err)), 78 | } 79 | }); 80 | 81 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 82 | 83 | false 84 | } 85 | } 86 | } 87 | } 88 | 89 | fn view_topic(topic_data: &Value) -> Html { 90 | let wish_val = &topic_data["randomWish"]; 91 | let random_wish = random_wish_node(wish_val); 92 | 93 | let topic = &topic_data["topicBySlug"]; 94 | let topic_name = topic["name"].as_str().unwrap(); 95 | let document = gloo_utils::document(); 96 | document.set_title(&format!( 97 | "{} - {}", 98 | topic_name, 99 | CFG.get("site.title").unwrap() 100 | )); 101 | 102 | let articles_vec = topic["articles"].as_array().unwrap(); 103 | let articles = 104 | articles_vec.iter().map(|article| article_card_node(article)); 105 | 106 | html! { 107 | <> 108 | { random_wish } 109 |
110 | { topic_name } 111 | { " 话题下,文章共 " } 112 | { topic["quotes"].as_i64().unwrap() } 113 | { " 篇:" } 114 |
115 | { for articles } 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /frontend-yew/src/show/topics.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use graphql_client::GraphQLQuery; 3 | use serde_json::{Value, json}; 4 | 5 | use crate::util::{ 6 | constant::CFG, 7 | common::{FetchState, fetch_gql_data}, 8 | }; 9 | use crate::components::nodes::{random_wish_node, topic_tag_node}; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "./graphql/schema.graphql", 14 | query_path = "./graphql/topics.graphql" 15 | )] 16 | struct TopicsData; 17 | 18 | async fn query_str() -> String { 19 | let build_query = TopicsData::build_query(topics_data::Variables {}); 20 | let query = json!(build_query); 21 | 22 | query.to_string() 23 | } 24 | 25 | pub enum Msg { 26 | SetState(FetchState), 27 | GetData, 28 | } 29 | 30 | pub struct Topics { 31 | data: FetchState, 32 | } 33 | 34 | impl Component for Topics { 35 | type Message = Msg; 36 | type Properties = (); 37 | 38 | fn create(_ctx: &Context) -> Self { 39 | Self { data: FetchState::NotFetching } 40 | } 41 | 42 | fn view(&self, _ctx: &Context) -> Html { 43 | match &self.data { 44 | FetchState::NotFetching => html! { "NotFetching" }, 45 | FetchState::Fetching => html! { "Fetching" }, 46 | FetchState::Success(topics_data) => view_topics(topics_data), 47 | FetchState::Failed(err) => html! { err }, 48 | } 49 | } 50 | 51 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 52 | if first_render { 53 | ctx.link().send_message(Msg::GetData); 54 | } 55 | } 56 | 57 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 58 | match msg { 59 | Msg::SetState(fetch_state) => { 60 | self.data = fetch_state; 61 | 62 | true 63 | } 64 | Msg::GetData => { 65 | ctx.link().send_future(async { 66 | match fetch_gql_data(&query_str().await).await { 67 | Ok(data) => Msg::SetState(FetchState::Success(data)), 68 | Err(err) => Msg::SetState(FetchState::Failed(err)), 69 | } 70 | }); 71 | 72 | ctx.link().send_message(Msg::SetState(FetchState::Fetching)); 73 | 74 | false 75 | } 76 | } 77 | } 78 | 79 | fn changed(&mut self, ctx: &Context) -> bool { 80 | ctx.link().send_message(Msg::GetData); 81 | 82 | false 83 | } 84 | } 85 | 86 | fn view_topics(topics_data: &Value) -> Html { 87 | let document = gloo_utils::document(); 88 | document.set_title(&format!( 89 | "{} - {}", 90 | "Topics", 91 | CFG.get("site.title").unwrap() 92 | )); 93 | 94 | let wish_val = &topics_data["randomWish"]; 95 | let random_wish = random_wish_node(wish_val); 96 | 97 | let topics_vec = topics_data["topics"].as_array().unwrap(); 98 | let topics = topics_vec.iter().map(|topic| topic_tag_node(topic)); 99 | 100 | html! { 101 | <> 102 | { random_wish } 103 |
104 | { for topics } 105 |
106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /frontend-yew/src/util/common.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{self, Debug, Display, Formatter}, 4 | }; 5 | use wasm_bindgen::{prelude::*, JsCast}; 6 | use wasm_bindgen_futures::JsFuture; 7 | use web_sys::{Request, RequestInit, RequestMode, Response}; 8 | use serde_json::{Value, from_str}; 9 | 10 | use crate::util::constant::CFG; 11 | 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct FetchError { 14 | err: JsValue, 15 | } 16 | 17 | impl Display for FetchError { 18 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 19 | Debug::fmt(&self.err, f) 20 | } 21 | } 22 | 23 | impl Error for FetchError {} 24 | 25 | impl From for FetchError { 26 | fn from(value: JsValue) -> Self { 27 | Self { err: value } 28 | } 29 | } 30 | 31 | pub enum FetchState { 32 | NotFetching, 33 | Fetching, 34 | Success(T), 35 | Failed(FetchError), 36 | } 37 | 38 | pub async fn fetch_gql_data(query: &str) -> Result { 39 | let mut req_opts = RequestInit::new(); 40 | req_opts.method("POST"); 41 | req_opts.body(Some(&JsValue::from_str(query))); 42 | req_opts.mode(RequestMode::Cors); 43 | 44 | let request = Request::new_with_str_and_init(&gql_uri().await, &req_opts)?; 45 | 46 | let window = gloo_utils::window(); 47 | let resp_value = 48 | JsFuture::from(window.fetch_with_request(&request)).await?; 49 | let resp: Response = resp_value.dyn_into().unwrap(); 50 | let resp_text = JsFuture::from(resp.text()?).await?; 51 | 52 | let data_str = resp_text.as_string().unwrap(); 53 | let data_value: Value = from_str(&data_str).unwrap(); 54 | 55 | Ok(data_value["data"].clone()) 56 | } 57 | 58 | pub async fn gql_uri() -> String { 59 | let addr = CFG.get("gql.addr").unwrap(); 60 | let path = CFG.get("gql.path").unwrap(); 61 | 62 | format!("{}/{}", addr, path) 63 | } 64 | -------------------------------------------------------------------------------- /frontend-yew/src/util/constant.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use toml::from_str; 3 | use lazy_static::lazy_static; 4 | 5 | use super::models::*; 6 | 7 | pub type ObjectId = String; 8 | 9 | lazy_static! { 10 | // CFG variables defined in cfg.toml file 11 | pub static ref CFG: HashMap<&'static str, String> = { 12 | let cfg_str = include_str!("../../.env.toml"); 13 | let config: Config = from_str(cfg_str).unwrap(); 14 | 15 | let mut map = HashMap::new(); 16 | 17 | map.insert("site.title", config.site.title); 18 | 19 | map.insert("gql.addr", config.gql.addr); 20 | map.insert("gql.path",config.gql.path); 21 | 22 | map.insert("theme_mode.title", config.theme_mode.title); 23 | map.insert("theme_mode.svg", config.theme_mode.svg); 24 | 25 | map.insert("i18n.title", config.i18n.title); 26 | map.insert("i18n.href", config.i18n.href); 27 | map.insert("i18n.svg",config.i18n.svg); 28 | 29 | map.insert("github.title", config.github.title); 30 | map.insert("github.href", config.github.href); 31 | map.insert("github.svg",config.github.svg); 32 | 33 | map 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /frontend-yew/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | pub mod routes; 4 | pub mod models; 5 | pub mod constant; 6 | pub mod common; 7 | -------------------------------------------------------------------------------- /frontend-yew/src/util/models.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct Config { 5 | pub site: Site, 6 | pub gql: Gql, 7 | pub theme_mode: ThemeMode, 8 | pub i18n: I18n, 9 | pub github: Github, 10 | } 11 | 12 | #[derive(Deserialize)] 13 | pub struct Site { 14 | pub title: String, 15 | } 16 | 17 | #[derive(Deserialize)] 18 | pub struct Gql { 19 | pub addr: String, 20 | pub path: String, 21 | } 22 | 23 | #[derive(Deserialize)] 24 | pub struct ThemeMode { 25 | pub title: String, 26 | pub svg: String, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct I18n { 31 | pub title: String, 32 | pub href: String, 33 | pub svg: String, 34 | } 35 | 36 | #[derive(Deserialize)] 37 | pub struct Github { 38 | pub title: String, 39 | pub href: String, 40 | pub svg: String, 41 | } 42 | -------------------------------------------------------------------------------- /frontend-yew/src/util/routes.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_router::prelude::*; 3 | 4 | use crate::show::{ 5 | home::Home, sign_in::SignIn, register::Register, author::Author, 6 | articles::Articles, categories::Categories, topics::Topics, 7 | article::Article, category::Category, topic::Topic, explore::Explore, 8 | }; 9 | use crate::manage::manage_index::ManageIndex; 10 | use crate::components::nodes::page_not_found; 11 | 12 | #[derive(Routable, PartialEq, Clone, Debug)] 13 | pub enum Routes { 14 | ///////// 15 | // nav // 16 | ///////// 17 | #[at("/")] 18 | Home, 19 | #[at("/sign-in")] 20 | SignIn, 21 | #[at("/register")] 22 | Register, 23 | #[at("/articles")] 24 | Articles, 25 | #[at("/categories")] 26 | Categories, 27 | #[at("/topics")] 28 | Topics, 29 | #[at("/explore")] 30 | Explore, 31 | /////////// 32 | // items // 33 | /////////// 34 | #[at("/:username")] 35 | Author { username: String }, 36 | #[at("/:username/:article_slug")] 37 | Article { username: String, article_slug: String }, 38 | #[at("/category/:category_slug")] 39 | Category { category_slug: String }, 40 | #[at("/topic/:topic_slug")] 41 | Topic { topic_slug: String }, 42 | //////////// 43 | // manage // 44 | //////////// 45 | #[at("/manage")] 46 | ManageIndex, 47 | //////////// 48 | // erros // 49 | //////////// 50 | #[not_found] 51 | #[at("/404")] 52 | NotFound, 53 | } 54 | 55 | pub fn switch(goal: &Routes) -> Html { 56 | match goal { 57 | ///////// 58 | // nav // 59 | ///////// 60 | Routes::Home => { 61 | html! { } 62 | } 63 | Routes::SignIn => { 64 | html! { } 65 | } 66 | Routes::Register => { 67 | html! { } 68 | } 69 | Routes::Articles => { 70 | html! { } 71 | } 72 | Routes::Categories => { 73 | html! { } 74 | } 75 | Routes::Topics => { 76 | html! { } 77 | } 78 | Routes::Explore => { 79 | html! { } 80 | } 81 | /////////// 82 | // items // 83 | /////////// 84 | Routes::Author { username } => { 85 | html! { } 86 | } 87 | Routes::Article { username, article_slug } => { 88 | html! {
} 89 | } 90 | Routes::Category { category_slug } => { 91 | html! { } 92 | } 93 | Routes::Topic { topic_slug } => { 94 | html! { } 95 | } 96 | //////////// 97 | // manage // 98 | //////////// 99 | Routes::ManageIndex => { 100 | html! { } 101 | } 102 | //////////// 103 | // erros // 104 | //////////// 105 | Routes::NotFound => page_not_found(), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /frontend-yew/trunk.toml: -------------------------------------------------------------------------------- 1 | [serve] 2 | port = 4000 3 | open = true 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | 3 | max_width = 80 4 | array_width = 50 5 | indent_style = "Block" 6 | reorder_imports = false 7 | reorder_modules = false 8 | merge_derives = false 9 | use_small_heuristics = "Max" 10 | 11 | error_on_line_overflow = true 12 | error_on_unformatted = true 13 | 14 | ignore = [] 15 | --------------------------------------------------------------------------------