mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6c7602123 |
No files matched your search
@@ -1,16 +1,34 @@
|
|||||||
# `Nocturne: Personal Website`
|
# `Nocturne: personal website & CMS`
|
||||||
|
|
||||||
This is the code repo of my [personal website](https://lumither.com),
|
> This is the last working version
|
||||||
and [here](https://github.com/Lumither/blog-posts/) is the link to the
|
|
||||||
content (blog posts etc.) repo.
|
This is the code repo of my personal website, and [here](https://github.com/Lumither/blog-posts/) is the link of the
|
||||||
|
content repo.
|
||||||
|
|
||||||
Powered by Next.js and Rust, built with passion and love
|
Powered by Next.js and Rust, built with passion and love
|
||||||
|
|
||||||
> [!NOTE]
|
*will add more here in the future
|
||||||
> This system is not production ready, and contains bugs, (security risk potentially).
|
|
||||||
> But have fun on it =)
|
|
||||||
|
|
||||||
# Todos
|
## Nav:
|
||||||
|
|
||||||
|
<todo>
|
||||||
|
|
||||||
|
## Todos:
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- [ ] Add: log system
|
||||||
|
- [ ] Add: search api
|
||||||
|
- [ ] Add: tag filter (api)
|
||||||
|
- [ ] Abandon: UUID mechanism
|
||||||
|
- [ ] Fix: db connection issue
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- [ ] Page: Home
|
||||||
|
- [ ] Page: About
|
||||||
|
- [ ] Page: Tags
|
||||||
|
- [ ] Page: Archive
|
||||||
|
- [ ] Page: Search
|
||||||
|
- [ ] Add: Right sidebar
|
||||||
|
|
||||||
- [ ] the todo list lol
|
|
||||||
- [ ] and much more...
|
|
||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"server",
|
"api_server",
|
||||||
|
"constants",
|
||||||
"macros",
|
"macros",
|
||||||
"markdown",
|
"markdown",
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "api_server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
macros = { path = "../macros" }
|
||||||
|
constants = { path = "../constants" }
|
||||||
|
markdown = { path = "../markdown" }
|
||||||
|
chrono = { version = "0.4.38", features = ["serde", "alloc"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] }
|
||||||
|
tracing-appender = "0.2.3"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
dirs = "5.0.1"
|
||||||
|
tokio = { version = "1.40.0", features = ["full"] }
|
||||||
|
sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono", "uuid"] }
|
||||||
|
axum = { version = "0.7.5", features = ["macros"] }
|
||||||
|
serde = { version = "1.0.199", features = ["derive"] }
|
||||||
|
serde_json = "1.0.128"
|
||||||
|
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
|
||||||
|
futures = { version = "0.3.30", features = ["executor"] }
|
||||||
|
cron = "0.13.0"
|
||||||
|
git2 = "0.19.0"
|
||||||
|
thiserror = "2.0.3"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use constants::blog::PAGE_SIZE;
|
||||||
|
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::Json;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use sqlx::{query, PgPool, Row};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub async fn get_page_count(
|
||||||
|
State(db_connection): State<PgPool>,
|
||||||
|
) -> Result<Json<Value>, Json<Value>> {
|
||||||
|
match query("select cast(ceil(count(*) / $1) as integer) as res from post")
|
||||||
|
.bind(PAGE_SIZE as f32)
|
||||||
|
.fetch_one(&db_connection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(res) => {
|
||||||
|
let res: i32 = res.get("res");
|
||||||
|
Ok(Json::from(json!({"res": res})))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get blog page count: {}", e.to_string());
|
||||||
|
Err(Json::from(json!({"error": e.to_string()})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use sqlx::{query, Error, PgPool, Row};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub async fn get_post(
|
||||||
|
State(db_connection): State<PgPool>,
|
||||||
|
Path(post_id): Path<String>,
|
||||||
|
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
|
||||||
|
let post_id = match Uuid::from_str(post_id.as_str()) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Failed to parse UUID <{}>: {}",
|
||||||
|
post_id.as_str(),
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json::from(json!(("error", e.to_string()))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let post = match query(
|
||||||
|
r#"
|
||||||
|
SELECT jsonb_strip_nulls(to_jsonb(t.*)) - 'post_id' AS post
|
||||||
|
FROM post t
|
||||||
|
WHERE post_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(post_id)
|
||||||
|
.fetch_one(&db_connection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(value) => value.get::<Value, _>("post"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Database error on Reading `base post information` from table `post`: <{}>: {}",
|
||||||
|
post_id.to_string(),
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
return match e {
|
||||||
|
Error::RowNotFound => Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json::from(json!({"error": e.to_string()})),
|
||||||
|
)),
|
||||||
|
_ => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json::from(json!({"error": e.to_string()})),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tags: Vec<_> = match query("SELECT tag FROM Tag WHERE post_id = $1;")
|
||||||
|
.bind(post_id)
|
||||||
|
.fetch_all(&db_connection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(tags) => tags.iter().map(|tag| tag.get::<String, _>("tag")).collect(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Database query error :{}", e.to_string());
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json::from(json!({"error": e.to_string()})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = match query(
|
||||||
|
r#"
|
||||||
|
SELECT jsonb_strip_nulls(to_jsonb(t.*)) - 'post_id' AS meta
|
||||||
|
FROM meta t
|
||||||
|
WHERE post_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(post_id)
|
||||||
|
.fetch_one(&db_connection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(value) => value.get::<Value, _>("meta"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Database error on Reading `{}` from table `{}`: <{}>: {}",
|
||||||
|
"post meta data",
|
||||||
|
"meta",
|
||||||
|
post_id.to_string(),
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json::from(json!({"error": e.to_string()})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let adjacent = match query(r"
|
||||||
|
WITH target_post AS (SELECT t.date FROM meta t WHERE post_id = $1),
|
||||||
|
prev AS (SELECT post_id, t.title, t.sub_title, t.header_img
|
||||||
|
FROM meta t
|
||||||
|
WHERE t.date < (SELECT date FROM target_post)
|
||||||
|
ORDER BY t.date DESC
|
||||||
|
LIMIT 1),
|
||||||
|
next AS (SELECT post_id, t.title, t.sub_title, t.header_img
|
||||||
|
FROM meta t
|
||||||
|
WHERE t.date > (SELECT date FROM target_post)
|
||||||
|
ORDER BY t.date
|
||||||
|
LIMIT 1)
|
||||||
|
SELECT json_strip_nulls(json_build_object(
|
||||||
|
'prev',
|
||||||
|
(SELECT json_build_object('id', post_id, 'title', title, 'sub_title', sub_title, 'header_img', header_img)
|
||||||
|
FROM prev),
|
||||||
|
'next',
|
||||||
|
(SELECT json_build_object('id', post_id, 'title', title, 'sub_title', sub_title, 'header_img', header_img)
|
||||||
|
FROM next)
|
||||||
|
)) AS result;
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.bind(post_id)
|
||||||
|
.fetch_one(&db_connection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(res) => {res.get::<Value, _>("result")}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Database error on Reading `{}` from table `{}`: <{}>: {}",
|
||||||
|
"adjoint posts data",
|
||||||
|
"meta",
|
||||||
|
post_id.to_string(),
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json::from(json!({"error": e.to_string()})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res = post.as_object().unwrap().to_owned();
|
||||||
|
res.insert("tags".to_string(), tags.into());
|
||||||
|
res.insert("meta".to_string(), meta);
|
||||||
|
res.insert("adj".to_string(), adjacent);
|
||||||
|
|
||||||
|
Ok(Json::from(json!(res)))
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use crate::constants::blog::PAGE_SIZE;
|
use constants::blog::PAGE_SIZE;
|
||||||
|
|
||||||
use axum::{extract::Query, extract::State, http::StatusCode, Json};
|
use axum::{extract::Query, extract::State, http::StatusCode, Json};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod get_page_count;
|
||||||
|
pub mod get_post;
|
||||||
|
pub mod get_post_list;
|
||||||
File renamed without changes.
+2
-4
@@ -1,10 +1,8 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{
|
use crate::blog::cron::check_update::utils::get_md_file_basename;
|
||||||
modules::blog::cron::check_update::utils::get_md_file_basename,
|
use crate::blog::cron::check_update::{error::PostIdxError, utils::parse_date};
|
||||||
modules::blog::cron::check_update::{error::PostIdxError, utils::parse_date},
|
|
||||||
};
|
|
||||||
use markdown::MdFile;
|
use markdown::MdFile;
|
||||||
|
|
||||||
use sqlx::{query, PgPool, Pool, Postgres, Row};
|
use sqlx::{query, PgPool, Pool, Postgres, Row};
|
||||||
+7
-9
@@ -1,28 +1,26 @@
|
|||||||
use std::{
|
use std::env;
|
||||||
env,
|
use std::fs::remove_dir_all;
|
||||||
fs::remove_dir_all,
|
use std::path::{Path, PathBuf};
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::config::general::{default_value, var_name},
|
blog::cron::check_update::{
|
||||||
modules::blog::cron::check_update::{
|
|
||||||
index::drop_index,
|
index::drop_index,
|
||||||
utils::{expand_path, get_md_file_basename, index_files, search_md},
|
utils::{expand_path, get_md_file_basename, index_files, search_md},
|
||||||
},
|
},
|
||||||
utils::git,
|
utils::git,
|
||||||
};
|
};
|
||||||
|
use constants::config::general::{default_value, var_name};
|
||||||
use macros::panic_with_log;
|
use macros::panic_with_log;
|
||||||
|
|
||||||
use git2::{Delta, Repository};
|
use git2::{Delta, Repository};
|
||||||
use sqlx::{PgPool, Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
use tracing::{error, info, warn, Level};
|
use tracing::{error, info, warn, Level};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod index;
|
mod index;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub fn task(db_connection: &PgPool) {
|
pub fn task(db_connection: &Pool<Postgres>) {
|
||||||
// init
|
// init
|
||||||
let git_work_dir = {
|
let git_work_dir = {
|
||||||
let path = if let Ok(default_log_path) = env::var(var_name::WORK_DIR) {
|
let path = if let Ok(default_log_path) = env::var(var_name::WORK_DIR) {
|
||||||
+9
-11
@@ -1,18 +1,16 @@
|
|||||||
|
use dirs::home_dir;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{
|
use crate::blog::cron::check_update::index::index_file;
|
||||||
constants::{
|
|
||||||
config::general::{default_value, var_name},
|
|
||||||
time::DEFAULT_DATE_FORMAT,
|
|
||||||
},
|
|
||||||
modules::blog::cron::check_update::index::index_file,
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, ParseResult};
|
use chrono::{DateTime, FixedOffset, ParseResult};
|
||||||
use dirs::home_dir;
|
use constants::config::general::{default_value, var_name};
|
||||||
use futures::{executor::block_on, future::join_all};
|
use constants::time::DEFAULT_DATE_FORMAT;
|
||||||
|
|
||||||
|
use futures::executor::block_on;
|
||||||
|
use futures::future::join_all;
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
use tokio::{runtime::Runtime, spawn};
|
use tokio::runtime::Runtime;
|
||||||
|
use tokio::spawn;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub fn search_md(entry: PathBuf) -> Vec<PathBuf> {
|
pub fn search_md(entry: PathBuf) -> Vec<PathBuf> {
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use crate::scheduler::tasks::basic::BasicTask;
|
||||||
|
use crate::scheduler::tasks::Task;
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
pub mod check_update;
|
||||||
|
|
||||||
|
pub fn get_tasks(db_connection: Pool<Postgres>) -> Vec<Box<dyn Task>> {
|
||||||
|
let db_conn_clone = db_connection.clone();
|
||||||
|
vec![
|
||||||
|
BasicTask::new(move || check_update::task(&db_conn_clone), "0/15 * * * * *")
|
||||||
|
.unwrap()
|
||||||
|
.to_task(),
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
mod api;
|
||||||
|
pub mod cron;
|
||||||
|
|
||||||
|
use crate::blog::api::{
|
||||||
|
get_page_count::get_page_count, get_post::get_post, get_post_list::get_post_list,
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::Router;
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
pub fn get_router() -> Router<Pool<Postgres>> {
|
||||||
|
Router::new()
|
||||||
|
.route("/get_post_list", get(get_post_list))
|
||||||
|
.route("/get_page_count", get(get_page_count))
|
||||||
|
.route("/get_post/:post_id", get(get_post))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use crate::blog;
|
||||||
|
use crate::scheduler::Scheduler;
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
pub fn start(db_pool: Pool<Postgres>) {
|
||||||
|
let scheduler = Scheduler::new();
|
||||||
|
let _ = vec![blog::cron::get_tasks(db_pool.clone())]
|
||||||
|
.into_iter()
|
||||||
|
.map(|list| scheduler.insert_list(list))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
scheduler.start().unwrap()
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@ use std::env;
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::constants::config::database::{default_value, var_name};
|
use constants::config::database::{default_value, var_name};
|
||||||
|
|
||||||
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
|
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
|
||||||
use sqlx::{ConnectOptions, Pool, Postgres};
|
use sqlx::{ConnectOptions, Pool, Postgres};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -13,7 +12,7 @@ pub async fn init() -> Result<Pool<Postgres>, Box<dyn Error>> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.disable_statement_logging();
|
.disable_statement_logging();
|
||||||
Ok(PgPoolOptions::new()
|
Ok(PgPoolOptions::new()
|
||||||
.max_connections(1) // todo: temporary solution, to be fixed
|
.max_connections(1) // todo: temporal solution, to be fixed
|
||||||
.connect_with(db_connect_option)
|
.connect_with(db_connect_option)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::{env, io};
|
use std::{env, io};
|
||||||
|
|
||||||
use crate::constants::config::general::{default_value, var_name};
|
|
||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
use constants::config::general::{default_value, var_name};
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
|
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
Layer, Registry, filter::LevelFilter, fmt, fmt::MakeWriter, fmt::format::Writer,
|
filter::LevelFilter, fmt, fmt::format::Writer, fmt::time::FormatTime, fmt::MakeWriter,
|
||||||
fmt::time::FormatTime, layer::SubscriberExt,
|
layer::SubscriberExt, Layer, Registry,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LocalTimer;
|
struct LocalTimer;
|
||||||
@@ -19,7 +18,7 @@ impl FormatTime for LocalTimer {
|
|||||||
write!(
|
write!(
|
||||||
w,
|
w,
|
||||||
"{}",
|
"{}",
|
||||||
Local::now().format(crate::constants::time::DEFAULT_TIME_FORMAT)
|
Local::now().format(constants::time::DEFAULT_TIME_FORMAT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -1,13 +1,13 @@
|
|||||||
|
mod cron;
|
||||||
mod database;
|
mod database;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
mod misc;
|
mod misc;
|
||||||
|
mod schema;
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
use macros::panic_with_log;
|
||||||
use std::{env, panic};
|
use std::{env, panic};
|
||||||
|
use tracing::{info, Level};
|
||||||
use crate::modules::{ModuleTree, blog::Blog};
|
|
||||||
use macros::error_panic;
|
|
||||||
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
pub fn load_env() {
|
pub fn load_env() {
|
||||||
if let Some(env_file) = env::args().nth(1) {
|
if let Some(env_file) = env::args().nth(1) {
|
||||||
@@ -34,17 +34,25 @@ pub async fn start() {
|
|||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error_panic!("database connection pool failed to initialize: {}", e);
|
panic_with_log!(
|
||||||
|
Level::ERROR,
|
||||||
|
"database connection pool failed to initialize: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// // schema init
|
// schema init
|
||||||
// schema::init(&db_pool).await;
|
schema::init(&db_pool).await;
|
||||||
|
|
||||||
ModuleTree::new()
|
// load cron task
|
||||||
.add_module(Blog {
|
cron::start(db_pool.clone());
|
||||||
db_handler: db_pool,
|
|
||||||
})
|
// api serve
|
||||||
.blocking_serve()
|
match server::start(db_pool).await {
|
||||||
.await
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
panic_with_log!(Level::ERROR, "server panicked: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use sqlx::{query, Pool, Postgres};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
const POST_BASE_MODEL: &str = r##"
|
||||||
|
CREATE TABLE IF NOT EXISTS Post
|
||||||
|
(
|
||||||
|
post_id UUID PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
category VARCHAR(255) NOT NULL,
|
||||||
|
filename TEXT UNIQUE NOT NULL,
|
||||||
|
summary TEXT,
|
||||||
|
sub_title TEXT,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
last_update TIMESTAMPTZ,
|
||||||
|
first_update TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
"##;
|
||||||
|
|
||||||
|
// base model, will add more cols at runtime
|
||||||
|
const POST_META_MODEL: &str = r##"
|
||||||
|
CREATE TABLE IF NOT EXISTS Meta
|
||||||
|
(
|
||||||
|
post_id UUID PRIMARY KEY
|
||||||
|
);
|
||||||
|
"##;
|
||||||
|
|
||||||
|
const POST_HASH_MODEL: &str = r##"
|
||||||
|
CREATE TABLE IF NOT EXISTS Hash
|
||||||
|
(
|
||||||
|
post_id UUID PRIMARY KEY,
|
||||||
|
hash VARCHAR(44)
|
||||||
|
);
|
||||||
|
"##;
|
||||||
|
|
||||||
|
const TAG_MODEL: &str = r##"
|
||||||
|
CREATE TABLE IF NOT EXISTS Tag
|
||||||
|
(
|
||||||
|
post_id UUID NOT NULL,
|
||||||
|
tag VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
"##;
|
||||||
|
|
||||||
|
pub async fn init(db_connection: &Pool<Postgres>) {
|
||||||
|
query(POST_BASE_MODEL).execute(db_connection).await.unwrap();
|
||||||
|
query(POST_META_MODEL).execute(db_connection).await.unwrap();
|
||||||
|
query(TAG_MODEL).execute(db_connection).await.unwrap();
|
||||||
|
query(POST_HASH_MODEL).execute(db_connection).await.unwrap();
|
||||||
|
info!("database schema initialized");
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::num::ParseIntError;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
use crate::blog;
|
||||||
|
use axum::Router;
|
||||||
|
use constants::config::server::{default_value, var_name};
|
||||||
|
use macros::panic_with_log;
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
use tracing::{error, info, warn, Level};
|
||||||
|
|
||||||
|
pub async fn start(db_pool: Pool<Postgres>) -> Result<(), Box<dyn Error>> {
|
||||||
|
let app = Router::new()
|
||||||
|
.nest("/blog", blog::get_router())
|
||||||
|
.with_state(db_pool);
|
||||||
|
|
||||||
|
let port: u32 = match env::var(var_name::PORT) {
|
||||||
|
Ok(value) => value.parse().unwrap_or_else(|e: ParseIntError| {
|
||||||
|
warn!(
|
||||||
|
"failed to parse `{}`, using default port: {}",
|
||||||
|
var_name::PORT,
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
default_value::PORT
|
||||||
|
}),
|
||||||
|
Err(_) => default_value::PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
let listener =
|
||||||
|
match tokio::net::TcpListener::bind(format!("{}:{}", default_value::HOST, port)).await {
|
||||||
|
Ok(listener) => {
|
||||||
|
info!("server started on {}:{}", default_value::HOST, port);
|
||||||
|
listener
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic_with_log!(
|
||||||
|
Level::ERROR,
|
||||||
|
"failed to start server on {}:{}: {}",
|
||||||
|
default_value::HOST,
|
||||||
|
port,
|
||||||
|
e.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
axum::serve(listener, app).await.unwrap_or_else(|e| {
|
||||||
|
error!("failed to start axum: {}", e.to_string());
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
mod blog;
|
||||||
|
mod init;
|
||||||
|
mod scheduler;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
init::load_env();
|
||||||
|
|
||||||
|
let _guards = init::logger::init();
|
||||||
|
|
||||||
|
init::start().await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
use crate::{
|
||||||
|
scheduler::scheduler_error::SchedulerError,
|
||||||
|
scheduler::scheduler_error::SchedulerError::IdAlreadyRunning,
|
||||||
|
scheduler::scheduler_error::SchedulerError::IdNotFound, scheduler::sleeper::SkippableSleeper,
|
||||||
|
scheduler::tasks::Task,
|
||||||
|
};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::{collections::HashMap, sync::Arc, thread};
|
||||||
|
use tracing::log::error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub mod scheduler_error;
|
||||||
|
mod sleeper;
|
||||||
|
mod task_func;
|
||||||
|
pub mod tasks;
|
||||||
|
|
||||||
|
struct TaskStatus {
|
||||||
|
sleeper: Option<Arc<SkippableSleeper>>,
|
||||||
|
is_alive: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Scheduler {
|
||||||
|
tasks: Arc<RwLock<HashMap<Uuid, Arc<Box<dyn Task>>>>>,
|
||||||
|
running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scheduler {
|
||||||
|
/// Start all tasks in the scheduler
|
||||||
|
pub fn start(&self) -> Result<(), SchedulerError> {
|
||||||
|
for (k, _v) in self.tasks.read()?.iter() {
|
||||||
|
self.run_non_blocking(k)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminate all tasks in the scheduler
|
||||||
|
pub fn halt() {}
|
||||||
|
|
||||||
|
fn run_task(
|
||||||
|
running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>,
|
||||||
|
id: Uuid,
|
||||||
|
task: Arc<Box<dyn Task>>,
|
||||||
|
) -> Result<(), SchedulerError> {
|
||||||
|
if running.read()?.get(&id).is_some() {
|
||||||
|
return Err(IdAlreadyRunning);
|
||||||
|
} else {
|
||||||
|
running.write()?.insert(
|
||||||
|
id,
|
||||||
|
TaskStatus {
|
||||||
|
sleeper: None,
|
||||||
|
is_alive: AtomicBool::new(true),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
if let Some(next_execution) = task.get_next_execution() {
|
||||||
|
// guarantee is alive
|
||||||
|
let task_status = {
|
||||||
|
let running_reading_guard = running.read()?;
|
||||||
|
running_reading_guard.get(&id).cloned().clone()
|
||||||
|
};
|
||||||
|
if let Some(task_status) = task_status {
|
||||||
|
if !&task_status.is_alive.load(Ordering::SeqCst) {
|
||||||
|
{
|
||||||
|
let mut running_lock_guard = running.write()?;
|
||||||
|
running_lock_guard.remove(&id);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
let sleeper = Arc::from(SkippableSleeper::from(next_execution));
|
||||||
|
{
|
||||||
|
let mut running_lock_guard = running.write()?;
|
||||||
|
running_lock_guard.insert(
|
||||||
|
id,
|
||||||
|
TaskStatus {
|
||||||
|
sleeper: Some(sleeper.clone()),
|
||||||
|
is_alive: AtomicBool::new(true),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sleeper.start();
|
||||||
|
task.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("unexpected error ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a task with `id`
|
||||||
|
pub fn run(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
||||||
|
let id = id.to_owned();
|
||||||
|
if let Some(task) = self.tasks.read()?.get(&id) {
|
||||||
|
Self::run_task(self.running.clone(), id, task.clone())
|
||||||
|
} else {
|
||||||
|
Err(IdNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_non_blocking(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
||||||
|
let id = id.to_owned();
|
||||||
|
if let Some(task) = self.tasks.read()?.get(&id) {
|
||||||
|
let running_clone = self.running.clone();
|
||||||
|
let task_clone = task.clone();
|
||||||
|
thread::spawn(move || Self::run_task(running_clone, id, task_clone));
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(IdNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_running(&self, id: &Uuid) -> Option<Arc<TaskStatus>> {
|
||||||
|
let ret = self.running.read().unwrap().get(id).cloned();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop a task with `id`
|
||||||
|
pub fn stop(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
||||||
|
match self.get_running(id) {
|
||||||
|
None => Err(IdNotFound),
|
||||||
|
Some(status) => {
|
||||||
|
status.is_alive.store(false, Ordering::SeqCst);
|
||||||
|
if let Some(s) = &status.sleeper {
|
||||||
|
s.skip();
|
||||||
|
} else {
|
||||||
|
return Err(IdAlreadyRunning);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn now(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
||||||
|
match self.get_running(id) {
|
||||||
|
None => Err(IdNotFound),
|
||||||
|
Some(status) => {
|
||||||
|
if let Some(s) = &status.sleeper {
|
||||||
|
s.skip()
|
||||||
|
} else {
|
||||||
|
return Err(IdAlreadyRunning);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&self, task: Box<dyn Task>) -> Result<Uuid, SchedulerError> {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
self.tasks.write()?.insert(id, Arc::new(task));
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_list(&self, tasks: Vec<Box<dyn Task>>) -> Result<Vec<Uuid>, SchedulerError> {
|
||||||
|
tasks.into_iter().map(|t| self.insert(t)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::scheduler::tasks::basic::BasicTask;
|
||||||
|
use crate::scheduler::Scheduler;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run() -> Result<(), Box<dyn Error>> {
|
||||||
|
let scheduler = Scheduler::default();
|
||||||
|
let task = BasicTask::new(|| println!("test print"), "* * * * * *")?;
|
||||||
|
let id = scheduler.insert(Box::new(task))?;
|
||||||
|
for i in 1..=2 {
|
||||||
|
println!("Loop {} start", i);
|
||||||
|
println!("Run {}", i);
|
||||||
|
scheduler.run_non_blocking(&id)?;
|
||||||
|
thread::sleep(Duration::from_secs(4));
|
||||||
|
scheduler.stop(&id)?;
|
||||||
|
println!("Stop {}", i);
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
println!("Loop {} finished", i);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-7
@@ -1,12 +1,8 @@
|
|||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
fmt::{Debug, Display, Formatter},
|
|
||||||
sync::PoisonError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison;
|
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::sync::PoisonError;
|
||||||
|
|
||||||
// todo: refactor with thiserror
|
|
||||||
pub enum SchedulerError {
|
pub enum SchedulerError {
|
||||||
IdNotFound,
|
IdNotFound,
|
||||||
IdAlreadyRunning,
|
IdAlreadyRunning,
|
||||||
+3
-6
@@ -1,10 +1,7 @@
|
|||||||
use std::{
|
|
||||||
sync::{Arc, Condvar, Mutex},
|
|
||||||
thread,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct SkippableSleeper {
|
pub struct SkippableSleeper {
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub trait TaskFunc: Fn() + Send + Sync + 'static {}
|
||||||
|
|
||||||
|
impl<T> TaskFunc for T where T: Fn() + Send + Sync + Clone + 'static {}
|
||||||
+8
-8
@@ -1,9 +1,9 @@
|
|||||||
use std::{error::Error, pin::Pin, str::FromStr};
|
use crate::scheduler::task_func::TaskFunc;
|
||||||
|
use crate::scheduler::tasks::Task;
|
||||||
use crate::scheduler::{task_func::TaskFunc, tasks::CronTask};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use cron::Schedule;
|
use cron::Schedule;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub struct BasicTask {
|
pub struct BasicTask {
|
||||||
schedule: Schedule,
|
schedule: Schedule,
|
||||||
@@ -21,17 +21,17 @@ impl BasicTask {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_task(self) -> Box<dyn CronTask> {
|
pub fn to_task(self) -> Box<dyn Task> {
|
||||||
Box::new(self)
|
Box::new(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CronTask for BasicTask {
|
impl Task for BasicTask {
|
||||||
fn get_next_execution(&self) -> Option<DateTime<Utc>> {
|
fn get_next_execution(&self) -> Option<DateTime<Utc>> {
|
||||||
self.schedule.upcoming(Utc).next()
|
self.schedule.upcoming(Utc).next()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
fn call(&self) {
|
||||||
Box::pin(async { (self.func)() })
|
(self.func)()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
pub mod basic;
|
||||||
|
|
||||||
|
pub trait Task: Send + Sync {
|
||||||
|
fn get_next_execution(&self) -> Option<DateTime<Utc>>;
|
||||||
|
fn call(&self);
|
||||||
|
}
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
|
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
|
||||||
///
|
///
|
||||||
use git2::{
|
use git2::{
|
||||||
AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository, build,
|
build, AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub struct FileDelta {
|
pub struct FileDelta {
|
||||||
pub old_path: Option<PathBuf>,
|
pub old_path: Option<PathBuf>,
|
||||||
@@ -15,7 +16,6 @@ pub struct FileDelta {
|
|||||||
pub status: Delta,
|
pub status: Delta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn do_fetch<'a>(
|
fn do_fetch<'a>(
|
||||||
repo: &'a Repository,
|
repo: &'a Repository,
|
||||||
refs: &[&str],
|
refs: &[&str],
|
||||||
@@ -23,6 +23,8 @@ fn do_fetch<'a>(
|
|||||||
) -> Result<AnnotatedCommit<'a>, git2::Error> {
|
) -> Result<AnnotatedCommit<'a>, git2::Error> {
|
||||||
let mut fetch_option = FetchOptions::new();
|
let mut fetch_option = FetchOptions::new();
|
||||||
|
|
||||||
|
// Always fetch all tags.
|
||||||
|
// Perform a download and also update tips
|
||||||
fetch_option.download_tags(AutotagOption::All);
|
fetch_option.download_tags(AutotagOption::All);
|
||||||
remote.fetch(refs, Some(&mut fetch_option), None)?;
|
remote.fetch(refs, Some(&mut fetch_option), None)?;
|
||||||
|
|
||||||
@@ -30,7 +32,6 @@ fn do_fetch<'a>(
|
|||||||
repo.reference_to_annotated_commit(&fetch_head)
|
repo.reference_to_annotated_commit(&fetch_head)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn fast_forward(
|
fn fast_forward(
|
||||||
repo: &Repository,
|
repo: &Repository,
|
||||||
lb: &mut Reference,
|
lb: &mut Reference,
|
||||||
@@ -44,11 +45,16 @@ fn fast_forward(
|
|||||||
// println!("{}", msg);
|
// println!("{}", msg);
|
||||||
lb.set_target(rc.id(), &msg)?;
|
lb.set_target(rc.id(), &msg)?;
|
||||||
repo.set_head(&name)?;
|
repo.set_head(&name)?;
|
||||||
repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?;
|
repo.checkout_head(Some(
|
||||||
|
build::CheckoutBuilder::default()
|
||||||
|
// For some reason, the force is required to make the working directory actually get updated
|
||||||
|
// I suspect we should be adding some logic to handle dirty working directory states,
|
||||||
|
// but this is just an example so maybe not.
|
||||||
|
.force(),
|
||||||
|
))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn normal_merge(
|
fn normal_merge(
|
||||||
repo: &Repository,
|
repo: &Repository,
|
||||||
local: &AnnotatedCommit,
|
local: &AnnotatedCommit,
|
||||||
@@ -67,12 +73,13 @@ fn normal_merge(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
|
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
|
||||||
|
// now create the merge commit
|
||||||
let msg = format!("merge({}) -> {}", remote.id(), local.id());
|
let msg = format!("merge({}) -> {}", remote.id(), local.id());
|
||||||
let sig = repo.signature()?;
|
let sig = repo.signature()?;
|
||||||
let local_commit = repo.find_commit(local.id())?;
|
let local_commit = repo.find_commit(local.id())?;
|
||||||
let remote_commit = repo.find_commit(remote.id())?;
|
let remote_commit = repo.find_commit(remote.id())?;
|
||||||
|
|
||||||
|
// Do our merge commit and set the current branch head to that commit.
|
||||||
let _merge_commit = repo.commit(
|
let _merge_commit = repo.commit(
|
||||||
Some("HEAD"),
|
Some("HEAD"),
|
||||||
&sig,
|
&sig,
|
||||||
@@ -81,26 +88,34 @@ fn normal_merge(
|
|||||||
&result_tree,
|
&result_tree,
|
||||||
&[&local_commit, &remote_commit],
|
&[&local_commit, &remote_commit],
|
||||||
)?;
|
)?;
|
||||||
|
// Set working tree to match head.
|
||||||
repo.checkout_head(None)?;
|
repo.checkout_head(None)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn do_merge<'a>(
|
fn do_merge<'a>(
|
||||||
repo: &'a Repository,
|
repo: &'a Repository,
|
||||||
remote_branch: &str,
|
remote_branch: &str,
|
||||||
fetch_commit: AnnotatedCommit<'a>,
|
fetch_commit: AnnotatedCommit<'a>,
|
||||||
) -> Result<Vec<FileDelta>, git2::Error> {
|
) -> Result<Vec<FileDelta>, git2::Error> {
|
||||||
|
// 1. do a merge analysis
|
||||||
let analysis = repo.merge_analysis(&[&fetch_commit])?;
|
let analysis = repo.merge_analysis(&[&fetch_commit])?;
|
||||||
|
|
||||||
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
|
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
|
||||||
|
|
||||||
|
// 2. Do the appropriate merge
|
||||||
if analysis.0.is_fast_forward() {
|
if analysis.0.is_fast_forward() {
|
||||||
|
// println!("Doing a fast-forward");
|
||||||
|
// do a fast-forward
|
||||||
let ref_name = format!("refs/heads/{}", remote_branch);
|
let ref_name = format!("refs/heads/{}", remote_branch);
|
||||||
match repo.find_reference(&ref_name) {
|
match repo.find_reference(&ref_name) {
|
||||||
Ok(mut r) => {
|
Ok(mut r) => {
|
||||||
fast_forward(repo, &mut r, &fetch_commit)?;
|
fast_forward(repo, &mut r, &fetch_commit)?;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
// The branch doesn't exist, so set the reference to the
|
||||||
|
// commit directly. Usually this is because you are pulling
|
||||||
|
// into an empty repository.
|
||||||
repo.reference(
|
repo.reference(
|
||||||
&ref_name,
|
&ref_name,
|
||||||
fetch_commit.id(),
|
fetch_commit.id(),
|
||||||
@@ -117,12 +132,17 @@ fn do_merge<'a>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if analysis.0.is_normal() {
|
} else if analysis.0.is_normal() {
|
||||||
|
// do a normal merge
|
||||||
normal_merge(repo, &head_commit, &fetch_commit)?;
|
normal_merge(repo, &head_commit, &fetch_commit)?;
|
||||||
}
|
}
|
||||||
|
// else {
|
||||||
|
// // already up-to-date
|
||||||
|
// println!("Nothing to do...");
|
||||||
|
// }
|
||||||
|
|
||||||
calculate_diff(repo, &head_commit, &fetch_commit)
|
calculate_diff(repo, &head_commit, &fetch_commit)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn calculate_diff(
|
fn calculate_diff(
|
||||||
repo: &Repository,
|
repo: &Repository,
|
||||||
local: &AnnotatedCommit,
|
local: &AnnotatedCommit,
|
||||||
@@ -136,12 +156,23 @@ fn calculate_diff(
|
|||||||
|
|
||||||
if let Err(e) = diff.foreach(
|
if let Err(e) = diff.foreach(
|
||||||
&mut |d, _| {
|
&mut |d, _| {
|
||||||
|
// let delta = d.status();
|
||||||
|
//
|
||||||
|
// let n_path = d.new_file().path();
|
||||||
|
// let o_path = d.old_file().path();
|
||||||
|
//
|
||||||
|
// if let Some(path) = n_path {
|
||||||
|
// updated_files.insert(path.to_owned());
|
||||||
|
// }
|
||||||
|
// if let Some(path) = o_path {
|
||||||
|
// updated_files.insert(path.to_owned());
|
||||||
|
// }
|
||||||
updated_files.push(FileDelta {
|
updated_files.push(FileDelta {
|
||||||
old_path: d.old_file().path().map(PathBuf::from),
|
old_path: d.old_file().path().map(PathBuf::from),
|
||||||
new_path: d.new_file().path().map(PathBuf::from),
|
new_path: d.new_file().path().map(PathBuf::from),
|
||||||
status: d.status(),
|
status: d.status(),
|
||||||
});
|
});
|
||||||
true
|
true // Continue
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -153,7 +184,6 @@ fn calculate_diff(
|
|||||||
Ok(updated_files.into_iter().collect())
|
Ok(updated_files.into_iter().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn sync(
|
pub fn sync(
|
||||||
remote_name: &str,
|
remote_name: &str,
|
||||||
remote_branch: &str,
|
remote_branch: &str,
|
||||||
File renamed without changes.
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gateway"
|
name = "constants"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "macros"
|
name = "macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
#[macro_export]
|
|
||||||
macro_rules! dev_consume {
|
|
||||||
($($var:ident),+) => {
|
|
||||||
$(let _ = $var;)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/// Includes a UTF-8 encoded file as a string
|
|
||||||
///
|
|
||||||
/// The file is located relative to `CARGO_MANIFEST_DIR`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use macros::include_str_from_crate_root;
|
|
||||||
///
|
|
||||||
/// let str: &str = include_str_from_crate_root!("/query/scheme.sql");
|
|
||||||
///
|
|
||||||
// todo: impl
|
|
||||||
// const PATH_PREFIX: &str = "/src";
|
|
||||||
// let str: &str = include_str_from_crate_root!("{}/query/scheme.sql", PATH_PREFIX);
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! include_str_from_crate_root {
|
|
||||||
// ($path:literal $(, $($arg:tt)*)?) => {
|
|
||||||
// ($path:expr, $($args:tt)*) => {
|
|
||||||
// include_str!(concat!(env!("CARGO_MANIFEST_DIR"), format_args!($path, $($args)*)))
|
|
||||||
// };
|
|
||||||
($path:expr) => {
|
|
||||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), $path))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
mod dev;
|
|
||||||
mod include_str;
|
|
||||||
mod log;
|
mod log;
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
/// use macros::panic_with_log;
|
/// use macros::panic_with_log;
|
||||||
/// use tracing::Level;
|
/// use tracing::Level;
|
||||||
///
|
///
|
||||||
/// panic_with_log!(Level::ERROR, "test panicked: {}", 42);
|
/// panic_with_log!(Level::ERROR, "Test panicked: {}", 42);
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! panic_with_log {
|
macro_rules! panic_with_log {
|
||||||
@@ -18,25 +18,6 @@ macro_rules! panic_with_log {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// log the message with `Level::ERROR` and exit(1)
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
///
|
|
||||||
/// use macros::error_panic;
|
|
||||||
///
|
|
||||||
/// error_panic!("test panicked: {}", 42);
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! error_panic {
|
|
||||||
($msg:literal $(, $($arg:tt)*)?) => {{
|
|
||||||
use std::process;
|
|
||||||
use tracing::{event, Level};
|
|
||||||
event!(Level::ERROR, $msg $(, $($arg)*)?);
|
|
||||||
process::exit(1);
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
@@ -44,7 +25,7 @@ mod tests {
|
|||||||
use std::io;
|
use std::io;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::{Registry, fmt};
|
use tracing_subscriber::{fmt, Registry};
|
||||||
|
|
||||||
let format = tracing_subscriber::fmt::format()
|
let format = tracing_subscriber::fmt::format()
|
||||||
.with_level(true)
|
.with_level(true)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "markdown"
|
name = "markdown"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
|
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
|
||||||
serde = { version = "1.0.209", features = ["derive"] }
|
serde = { version = "1.0.209", features = ["derive"] }
|
||||||
serde_json = "1.0.128"
|
serde_json = "1.0.128"
|
||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
yaml-rust2 = "0.10.0"
|
yaml-rust2 = "0.9.0"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
thiserror = "2.0.3"
|
thiserror = "2.0.3"
|
||||||
@@ -8,7 +8,7 @@ use std::str::FromStr;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::utils::front_matter::{parse_front_matter, split_md_front_matter};
|
use crate::utils::front_matter::{parse_front_matter, split_md_front_matter};
|
||||||
|
|
||||||
use base64::{Engine, engine::general_purpose};
|
use base64::{engine::general_purpose, Engine};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "server"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
macros = { path = "../macros" }
|
|
||||||
markdown = { path = "../markdown" }
|
|
||||||
|
|
||||||
axum = { version = "0.8.1", features = ["macros"] }
|
|
||||||
tracing = { version = "0.1.41", features = ["log"] }
|
|
||||||
git2 = "0.20.0"
|
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
|
||||||
cron = "0.15.0"
|
|
||||||
sqlx = { version = "0.8.3", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono"] }
|
|
||||||
serde_json = "1.0.133"
|
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
|
||||||
tracing-subscriber = "0.3.18"
|
|
||||||
tracing-appender = "0.2.3"
|
|
||||||
dirs = "6.0.0"
|
|
||||||
thiserror = "2.0.11"
|
|
||||||
tokio = { version = "1.42.0", features = ["rt", "rt-multi-thread", "macros"] }
|
|
||||||
dotenv = "0.15.0"
|
|
||||||
uuid = { version = "1.13.2", features = ["v4"] }
|
|
||||||
futures = "0.3.31"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
git2 = "0.20.0"
|
|
||||||
chrono = "0.4.39"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
use chrono::Utc;
|
|
||||||
use git2::Repository;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let repo = Repository::open("../..").expect("failed to open repository");
|
|
||||||
let commit = repo
|
|
||||||
.head()
|
|
||||||
.expect("failed to get HEAD")
|
|
||||||
.peel_to_commit()
|
|
||||||
.expect("failed to get HEAD commit");
|
|
||||||
let hash = commit.id().to_string();
|
|
||||||
let utc_timestamp = Utc::now().date_naive().format("%Y%m%d").to_string();
|
|
||||||
let pkg_version = env!("CARGO_PKG_VERSION");
|
|
||||||
let build_id = format!("{}:{}@{}", pkg_version, &hash[..8], utc_timestamp);
|
|
||||||
println!("cargo:rustc-env=BUILD_ID={}", build_id);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#[allow(dead_code)]
|
|
||||||
mod constants;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod scheduler;
|
|
||||||
|
|
||||||
mod init;
|
|
||||||
mod modules;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use crate::init::{logger, start};
|
|
||||||
|
|
||||||
const BUILD_ID: &str = env!("BUILD_ID");
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
println!("{}", BUILD_ID);
|
|
||||||
|
|
||||||
init::load_env();
|
|
||||||
let _guards = logger::init();
|
|
||||||
|
|
||||||
start().await;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use crate::constants::blog::PAGE_SIZE;
|
|
||||||
|
|
||||||
use axum::{Json, extract::State};
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
use sqlx::{PgPool, Row, query};
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
const GET_PAGE_COUNT_QUERY: &str = include_str!("post_page_count.sql");
|
|
||||||
|
|
||||||
pub async fn handler(State(db_connection): State<PgPool>) -> Result<Json<Value>, Json<Value>> {
|
|
||||||
match query(GET_PAGE_COUNT_QUERY)
|
|
||||||
.bind(PAGE_SIZE as i64)
|
|
||||||
.fetch_one(&db_connection)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(res) => {
|
|
||||||
let res: i32 = res.get("count");
|
|
||||||
Ok(Json::from(json!({"res": res})))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("failed to get blog page count: {}", e.to_string());
|
|
||||||
Err(Json::from(json!({"error": e.to_string()})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
SELECT cast(ceil(count(*) / cast($1 AS FLOAT)) AS INTEGER) AS count
|
|
||||||
FROM posts;
|
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,34 +0,0 @@
|
|||||||
use macros::dev_consume;
|
|
||||||
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use axum::{
|
|
||||||
Json,
|
|
||||||
extract::{Path, State},
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
const GET_POST_BY_I64_ID: &str = include_str!("get_post_by_i64_id.sql");
|
|
||||||
const GET_POST_BY_STR_ID: &str = include_str!("get_post_by_str_id.sql");
|
|
||||||
|
|
||||||
pub async fn handler(
|
|
||||||
State(db_connection): State<PgPool>,
|
|
||||||
Path(identifier): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
if let Ok(id) = identifier.parse::<i64>() {
|
|
||||||
select_post_with_i64_id(&db_connection, id).await
|
|
||||||
} else {
|
|
||||||
select_post_with_str_id(&db_connection, &identifier).await
|
|
||||||
};
|
|
||||||
Json::from(json!({"res": "ok"})).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn select_post_with_i64_id(db: &PgPool, id: i64) {
|
|
||||||
dev_consume!(db, id);
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn select_post_with_str_id(db: &PgPool, id: &str) {
|
|
||||||
dev_consume!(db, id);
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
pub mod get_page_count;
|
|
||||||
|
|
||||||
// todo: remove exception tag
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub mod get_post;
|
|
||||||
// pub mod get_post_list;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// pub mod check_update;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
mod api;
|
|
||||||
mod cron;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
modules::{
|
|
||||||
blog::{
|
|
||||||
api::{
|
|
||||||
get_page_count,
|
|
||||||
get_post,
|
|
||||||
// get_post_list::get_post_list,
|
|
||||||
},
|
|
||||||
// cron::check_update,
|
|
||||||
},
|
|
||||||
Module,
|
|
||||||
},
|
|
||||||
scheduler::tasks::{async_basic::AsyncBasic, CronTask},
|
|
||||||
};
|
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
const MOUNT_POINT: &str = "/blog";
|
|
||||||
|
|
||||||
pub struct Blog {
|
|
||||||
pub db_handler: PgPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module for Blog {
|
|
||||||
fn get_server_router(&self) -> Router {
|
|
||||||
let db_handler = self.db_handler.clone();
|
|
||||||
// todo: move resource init to new func
|
|
||||||
// todo: remove debug comment
|
|
||||||
Router::new()
|
|
||||||
// .route("/get_post_list", get(get_post_list))
|
|
||||||
.route("/get_page_count", get(get_page_count::handler))
|
|
||||||
.route("/get_post/{identifier}", get(get_post::handler))
|
|
||||||
.with_state(db_handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mount_point(&self) -> &'static str {
|
|
||||||
MOUNT_POINT
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cron_tasks(&self) -> Vec<Box<dyn CronTask>> {
|
|
||||||
// let db_handler = self.db_handler.clone();
|
|
||||||
vec![
|
|
||||||
// BasicTask::new(move || check_update::task(&db_handler), "0/15 * * * * *")
|
|
||||||
// .unwrap()
|
|
||||||
// .to_task(),
|
|
||||||
AsyncBasic::new(|| Box::pin(async { println!("test") }), "* * * * * *")
|
|
||||||
.unwrap()
|
|
||||||
.to_task(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
CREATE TABLE tags
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(20) UNIQUE NOT NULL,
|
|
||||||
description TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE categories
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(20) UNIQUE NOT NULL,
|
|
||||||
description TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE posts
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
identifier VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
subtitle VARCHAR(255) NOT NULL,
|
|
||||||
published_date DATE NOT NULL,
|
|
||||||
last_update DATE,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
|
|
||||||
category SERIAL REFERENCES categories (id) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE post_tag
|
|
||||||
(
|
|
||||||
post SERIAL REFERENCES posts (id),
|
|
||||||
tag SERIAL REFERENCES tags (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE metadata
|
|
||||||
(
|
|
||||||
pid SERIAL REFERENCES posts (id) NOT NULL,
|
|
||||||
meta_key VARCHAR(50) NOT NULL,
|
|
||||||
meta_value TEXT NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT meta_pk PRIMARY KEY (pid, meta_key)
|
|
||||||
);
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
pub mod blog;
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::num::ParseIntError;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
constants::config::server::{default_value, var_name},
|
|
||||||
scheduler::{tasks::CronTask, Scheduler},
|
|
||||||
};
|
|
||||||
use macros::error_panic;
|
|
||||||
|
|
||||||
use axum::Router;
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
// todo: merge `get_server_router` & `get_mount_point`, make Option
|
|
||||||
pub trait Module {
|
|
||||||
fn get_server_router(&self) -> Router;
|
|
||||||
|
|
||||||
fn get_mount_point(&self) -> &'static str;
|
|
||||||
|
|
||||||
fn get_cron_tasks(&self) -> Vec<Box<dyn CronTask>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ModuleTree {
|
|
||||||
pub modules: Vec<Box<dyn Module>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModuleTree {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
ModuleTree { modules: vec![] }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_module(&mut self, module: impl Module + 'static) -> &mut Self {
|
|
||||||
self.modules.push(Box::new(module));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn blocking_serve(&self) {
|
|
||||||
let scheduler = Scheduler::new();
|
|
||||||
let mut app = Router::new();
|
|
||||||
|
|
||||||
let port: u32 = match env::var(var_name::PORT) {
|
|
||||||
Ok(value) => value.parse().unwrap_or_else(|e: ParseIntError| {
|
|
||||||
warn!(
|
|
||||||
"failed to parse `{}`, using default port: {}",
|
|
||||||
var_name::PORT,
|
|
||||||
e.to_string()
|
|
||||||
);
|
|
||||||
default_value::PORT
|
|
||||||
}),
|
|
||||||
Err(_) => default_value::PORT,
|
|
||||||
};
|
|
||||||
|
|
||||||
let listener = match TcpListener::bind(format!("{}:{}", default_value::HOST, port)).await {
|
|
||||||
Ok(listener) => {
|
|
||||||
info!("server started on {}:{}", default_value::HOST, port);
|
|
||||||
listener
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error_panic!(
|
|
||||||
"failed to start server on {}:{}: {}",
|
|
||||||
default_value::HOST,
|
|
||||||
port,
|
|
||||||
e.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for module in &self.modules {
|
|
||||||
let _ = scheduler.insert_list(module.get_cron_tasks());
|
|
||||||
app = app.nest(module.get_mount_point(), module.get_server_router());
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduler.start().unwrap();
|
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap_or_else(|e| {
|
|
||||||
error_panic!("failed to start axum: {}", e.to_string());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::scheduler::{
|
|
||||||
runner::Runner,
|
|
||||||
scheduler_error::{SchedulerError, SchedulerError::IdNotFound},
|
|
||||||
tasks::CronTask,
|
|
||||||
};
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod runner;
|
|
||||||
pub mod scheduler_error;
|
|
||||||
mod sleeper;
|
|
||||||
mod task_func;
|
|
||||||
pub mod tasks;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Scheduler {
|
|
||||||
running: Arc<RwLock<HashSet<Uuid>>>,
|
|
||||||
parking_pool: Arc<RwLock<HashMap<Uuid, Arc<Box<dyn CronTask>>>>>,
|
|
||||||
runner: Runner,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scheduler {
|
|
||||||
/// Start all tasks in the scheduler
|
|
||||||
pub fn start(&self) -> Result<(), SchedulerError> {
|
|
||||||
self.runner.run_all()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Terminate all tasks in the scheduler
|
|
||||||
pub fn halt(&self) -> Result<(), SchedulerError> {
|
|
||||||
for id in self.running.read()?.iter() {
|
|
||||||
let func = &self.runner.terminate_remove(*id)?;
|
|
||||||
self.running.write()?.remove(id);
|
|
||||||
self.parking_pool.write()?.insert(*id, func.clone());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a task with `id`
|
|
||||||
pub fn run(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
|
||||||
if self.running.read()?.contains(id) {
|
|
||||||
Ok(())
|
|
||||||
} else if self.parking_pool.read()?.contains_key(id) {
|
|
||||||
let func = self.parking_pool.write()?.remove(id).unwrap();
|
|
||||||
self.running.write()?.insert(*id);
|
|
||||||
self.runner.add_with_id(func, id)
|
|
||||||
} else {
|
|
||||||
Err(IdNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop a task with `id`
|
|
||||||
pub fn stop(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
|
||||||
dbg!("stop() called");
|
|
||||||
if self.running.read()?.contains(id) {
|
|
||||||
dbg!("b1");
|
|
||||||
let func = self.runner.terminate_remove(*id)?;
|
|
||||||
dbg!("b1a");
|
|
||||||
self.running.write()?.remove(id);
|
|
||||||
dbg!("b1b");
|
|
||||||
self.parking_pool.write()?.insert(*id, func.clone());
|
|
||||||
dbg!("b1c");
|
|
||||||
Ok(())
|
|
||||||
} else if self.parking_pool.read()?.get(id).is_some() {
|
|
||||||
dbg!("b2");
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
dbg!("b3");
|
|
||||||
Err(IdNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn now(&self, id: &Uuid) -> Result<(), SchedulerError> {
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn insert(&self, task: Box<dyn CronTask>) -> Result<Uuid, SchedulerError> {
|
|
||||||
let id = self.runner.add(task.into())?;
|
|
||||||
self.running.write()?.insert(id);
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_list(&self, tasks: Vec<Box<dyn CronTask>>) -> Result<Vec<Uuid>, SchedulerError> {
|
|
||||||
let ids = self
|
|
||||||
.runner
|
|
||||||
.add_list(tasks.into_iter().map(|t| t.into()).collect())?;
|
|
||||||
self.running.write()?.extend(&ids);
|
|
||||||
Ok(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::scheduler::tasks::basic::BasicTask;
|
|
||||||
use crate::scheduler::Scheduler;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_run() -> Result<(), Box<dyn Error>> {
|
|
||||||
let scheduler = Scheduler::default();
|
|
||||||
let task = BasicTask::new(|| println!("test print"), "* * * * * *")?;
|
|
||||||
let id = scheduler.insert(Box::new(task))?;
|
|
||||||
scheduler.start()?;
|
|
||||||
for i in 1..=2 {
|
|
||||||
println!("Loop {} start", i);
|
|
||||||
println!("Run {}", i);
|
|
||||||
scheduler.run(&id)?;
|
|
||||||
println!("Sleep {}", i);
|
|
||||||
thread::sleep(Duration::from_secs(4));
|
|
||||||
println!("Sleep {} finished", i);
|
|
||||||
scheduler.stop(&id)?;
|
|
||||||
println!("Stop {}", i);
|
|
||||||
thread::sleep(Duration::from_secs(2));
|
|
||||||
println!("Loop {} finished", i);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicBool, Ordering}, Arc,
|
|
||||||
RwLock,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::scheduler::{
|
|
||||||
scheduler_error::{SchedulerError, SchedulerError::IdNotFound},
|
|
||||||
sleeper::SkippableSleeper,
|
|
||||||
tasks::CronTask,
|
|
||||||
};
|
|
||||||
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
struct TaskStatus {
|
|
||||||
sleeper: RwLock<SkippableSleeper>,
|
|
||||||
is_running: AtomicBool,
|
|
||||||
expected_running: AtomicBool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TaskEntity {
|
|
||||||
func: Arc<Box<dyn CronTask>>,
|
|
||||||
status: TaskStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskEntity {
|
|
||||||
pub fn new(func: Arc<Box<dyn CronTask>>) -> Self {
|
|
||||||
let next_execution: SkippableSleeper = func.get_next_execution().unwrap().into();
|
|
||||||
Self {
|
|
||||||
func,
|
|
||||||
status: TaskStatus {
|
|
||||||
sleeper: next_execution.into(),
|
|
||||||
is_running: false.into(),
|
|
||||||
expected_running: true.into(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_blocking(&self) {
|
|
||||||
loop {
|
|
||||||
if self.status.expected_running.load(Ordering::SeqCst) {
|
|
||||||
self.status.sleeper.read().unwrap().start();
|
|
||||||
self.func.call().await;
|
|
||||||
if let Some(next_execution) = self.func.get_next_execution() {
|
|
||||||
*(self.status.sleeper.write().unwrap()) = next_execution.into();
|
|
||||||
} else {
|
|
||||||
self.status.is_running.store(false, Ordering::SeqCst);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminate(&self) {
|
|
||||||
self.status.expected_running.store(false, Ordering::SeqCst);
|
|
||||||
self.status.is_running.store(false, Ordering::SeqCst);
|
|
||||||
self.status.sleeper.read().unwrap().skip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Runner {
|
|
||||||
task_list: RwLock<HashMap<Uuid, Arc<TaskEntity>>>,
|
|
||||||
rt: Runtime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Runner {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
task_list: HashMap::new().into(),
|
|
||||||
rt: Runtime::new().unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_all(&self) -> Result<(), SchedulerError> {
|
|
||||||
let list = &self.task_list.read()?;
|
|
||||||
self.rt.block_on(async {
|
|
||||||
for (_, entity) in list.iter() {
|
|
||||||
if !entity.status.is_running.load(Ordering::SeqCst)
|
|
||||||
&& entity.status.expected_running.load(Ordering::SeqCst)
|
|
||||||
{
|
|
||||||
entity.status.is_running.store(true, Ordering::SeqCst);
|
|
||||||
tokio::spawn({
|
|
||||||
let entity_clone = entity.clone();
|
|
||||||
async move { entity_clone.run_blocking().await }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_with_id(
|
|
||||||
&self,
|
|
||||||
task: Arc<Box<dyn CronTask>>,
|
|
||||||
id: &Uuid,
|
|
||||||
) -> Result<(), SchedulerError> {
|
|
||||||
self.task_list
|
|
||||||
.write()?
|
|
||||||
.insert(*id, TaskEntity::new(task).into());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&self, task: Arc<Box<dyn CronTask>>) -> Result<Uuid, SchedulerError> {
|
|
||||||
let id = Uuid::new_v4();
|
|
||||||
self.add_with_id(task, &id)?;
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_list_with_id(
|
|
||||||
&self,
|
|
||||||
tasks: Vec<(&Uuid, Arc<Box<dyn CronTask>>)>,
|
|
||||||
) -> Result<(), SchedulerError> {
|
|
||||||
tasks
|
|
||||||
.into_iter()
|
|
||||||
.try_for_each(|(id, task)| self.add_with_id(task, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_list(
|
|
||||||
&self,
|
|
||||||
tasks: Vec<Arc<Box<dyn CronTask>>>,
|
|
||||||
) -> Result<Vec<Uuid>, SchedulerError> {
|
|
||||||
tasks
|
|
||||||
.into_iter()
|
|
||||||
.map(|task| {
|
|
||||||
let id = Uuid::new_v4();
|
|
||||||
self.add_with_id(task, &id).map(move |_| id)
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminate_remove(&self, id: Uuid) -> Result<Arc<Box<dyn CronTask>>, SchedulerError> {
|
|
||||||
match self.task_list.read()?.get(&id) {
|
|
||||||
None => Err(IdNotFound),
|
|
||||||
Some(task) => {
|
|
||||||
task.terminate();
|
|
||||||
let func = &self.task_list.write()?.remove(&id).unwrap().func;
|
|
||||||
Ok(func.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Runner {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
pub trait TaskFunc: Fn() + Send + Sync + 'static {}
|
|
||||||
|
|
||||||
impl<T> TaskFunc for T where T: Fn() + Send + Sync + Clone + 'static {}
|
|
||||||
|
|
||||||
pub trait AsyncTaskFunc:
|
|
||||||
Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> AsyncTaskFunc for T where
|
|
||||||
T: Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync + Clone + 'static
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
use std::{error::Error, pin::Pin, str::FromStr};
|
|
||||||
|
|
||||||
use crate::{scheduler::task_func::AsyncTaskFunc, scheduler::tasks::CronTask};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use cron::Schedule;
|
|
||||||
|
|
||||||
pub struct AsyncBasic {
|
|
||||||
schedule: Schedule,
|
|
||||||
func: Box<dyn AsyncTaskFunc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncBasic {
|
|
||||||
pub fn new<F>(func: F, cron_expr: &str) -> Result<Self, Box<dyn Error>>
|
|
||||||
where
|
|
||||||
F: AsyncTaskFunc,
|
|
||||||
{
|
|
||||||
Ok(AsyncBasic {
|
|
||||||
schedule: Schedule::from_str(cron_expr)?,
|
|
||||||
func: Box::new(func),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_task(self) -> Box<dyn CronTask> {
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CronTask for AsyncBasic {
|
|
||||||
fn get_next_execution(&self) -> Option<DateTime<Utc>> {
|
|
||||||
self.schedule.upcoming(Utc).next()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&self) -> Pin<Box<(dyn Future<Output = ()> + Send)>> {
|
|
||||||
Box::pin((self.func)())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
pub mod async_basic;
|
|
||||||
pub mod basic;
|
|
||||||
|
|
||||||
pub trait CronTask: Send + Sync {
|
|
||||||
fn get_next_execution(&self) -> Option<DateTime<Utc>>;
|
|
||||||
fn call(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, CardBody } from '@nextui-org/card';
|
|
||||||
import Headers from '@/app/(pages)/blog/post/[id]/Headers';
|
|
||||||
import Footer from '@/app/(pages)/blog/post/[id]/Footer';
|
|
||||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
post: any
|
|
||||||
}
|
|
||||||
|
|
||||||
async function PostCard(props: Props) {
|
|
||||||
let post = props.post;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Card className={ `max-w-full relative` }>
|
|
||||||
|
|
||||||
{/* card headers */ }
|
|
||||||
<Headers title={ post.title }
|
|
||||||
subtitle={ post.sub_title }
|
|
||||||
summary={ post.summary }
|
|
||||||
category={ post.category }
|
|
||||||
tags={ post.tags }
|
|
||||||
first_update={ post.first_update }
|
|
||||||
header_img={ post.meta.header_img }
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* card body */ }
|
|
||||||
<CardBody>
|
|
||||||
<div className={ 'p-5' }>
|
|
||||||
<MarkdownRenderer>
|
|
||||||
{ post.content }
|
|
||||||
</MarkdownRenderer>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
|
|
||||||
|
|
||||||
{/* card footer */ }
|
|
||||||
<Footer
|
|
||||||
adjacent_posts={ post.adj }
|
|
||||||
perm_link={ post.meta.link }
|
|
||||||
author={ post.meta.author }
|
|
||||||
author_link={ post.meta.author_link }
|
|
||||||
first_update={ post.first_update }
|
|
||||||
last_update={ post.last_update }
|
|
||||||
cc={ post.meta.cc }
|
|
||||||
post_id={ post.meta.id }
|
|
||||||
title={ post.title }
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostCard;
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import React, { cache } from 'react';
|
|
||||||
import PostCard from '@/app/(pages)/blog/post/[id]/PostCard';
|
|
||||||
import ButtonGoBack from '@/app/(pages)/blog/post/[id]/ButtonGoBack';
|
|
||||||
import * as motion from 'motion/react-client';
|
|
||||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { SITE_CONFIG } from '@/src/constants';
|
|
||||||
|
|
||||||
const fetchPost = cache(async (id: string) => {
|
|
||||||
const res = await fetchNocturneApi(`/blog/get_post/${ id }`);
|
|
||||||
// throw Error('Not Found');
|
|
||||||
return await res.json();
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function generateMetadata(
|
|
||||||
{ params }: { params: Promise<{ id: string }> })
|
|
||||||
: Promise<Metadata> {
|
|
||||||
try {
|
|
||||||
const title = (await fetchPost((await params).id)).title as string;
|
|
||||||
return {
|
|
||||||
title: `${ title } - ${ SITE_CONFIG.name }`
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
title: 'Unexpected Error'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function BlogReader({
|
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: Promise<{ id: string }>
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
let post = await fetchPost((await params).id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className={ `max-w-full` }
|
|
||||||
initial={ { y: 20, opacity: 0 } }
|
|
||||||
animate={ { y: 0, opacity: 1 } }
|
|
||||||
transition={ { ease: 'easeInOut', duration: 0.5 } }
|
|
||||||
>
|
|
||||||
<div className={ `max-w-full` }>
|
|
||||||
<PostCard post={ post } />
|
|
||||||
</div>
|
|
||||||
<div className={ 'mt-3' }>
|
|
||||||
<ButtonGoBack />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
return (
|
|
||||||
<div className={ `flex h-full w-full justify-center items-center` }>
|
|
||||||
<div className={ `flex-row` }>
|
|
||||||
<p className={ `text-xl font-bold` }>
|
|
||||||
{ 'Unexpected Error:' }
|
|
||||||
</p>
|
|
||||||
<p className={ `font-bold` }>
|
|
||||||
{ e.message }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlogReader;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Navbar from '@/components/Navbar';
|
|
||||||
import Footer from '@/components/Footer';
|
|
||||||
|
|
||||||
import * as motion from 'motion/react-client';
|
|
||||||
|
|
||||||
function Layout({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<motion.div
|
|
||||||
initial={ { opacity: 0 } }
|
|
||||||
animate={ { opacity: 1 } }
|
|
||||||
transition={ { ease: 'easeInOut', duration: 0.3, delay: 0.5 } }
|
|
||||||
className={ `justify-center flex flex-row w-full max-w-full` }>
|
|
||||||
<Navbar />
|
|
||||||
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
|
|
||||||
{ children }
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Activity } from '@/src/HomePageActivities/activity';
|
|
||||||
import { getGhActivities } from '@/src/HomePageActivities/ghActivity';
|
|
||||||
import * as motion from 'motion/react-client';
|
|
||||||
|
|
||||||
const Activities = () => {
|
|
||||||
|
|
||||||
const [ activities, setActivities ] = useState<Activity[] | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getGhActivities().then(data => setActivities(data));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!activities) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>loading</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ul className={ 'flex flex-col gap-y-3 items-end' }>
|
|
||||||
{ activities.map((activity, key) => (
|
|
||||||
<motion.div
|
|
||||||
key={ key }
|
|
||||||
initial={ { opacity: 0, y: 20 } }
|
|
||||||
animate={ { opacity: 1, y: 0 } }
|
|
||||||
transition={ { ease: 'easeInOut', duration: 0.2 } }
|
|
||||||
>
|
|
||||||
<li className={ 'backdrop-blur bg-white/75 dark:bg-black/45 rounded-xl w-fit' }>
|
|
||||||
{ activity.toElement() }
|
|
||||||
</li>
|
|
||||||
</motion.div>
|
|
||||||
)) }
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Activities;
|
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { SITE_CONFIG } from '@/src/constants';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: `About - ${ SITE_CONFIG.name }`
|
|
||||||
};
|
|
||||||
|
|
||||||
function Layout({
|
function Layout({
|
||||||
children
|
children
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||||
import { fetchText } from '@/app/fetch';
|
import { fetchText } from '@/app/fetch';
|
||||||
|
|
||||||
const Me = async () => {
|
const Me = async () => {
|
||||||
File renamed without changes.
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||||
import { fetchText } from '@/app/fetch';
|
import { fetchText } from '@/app/fetch';
|
||||||
|
|
||||||
const Website = async () => {
|
const Website = async () => {
|
||||||
File renamed without changes.
File renamed without changes.
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pagination as NextUIPagination } from '@nextui-org/react';
|
import { Pagination as NextUIPagination } from '@nextui-org/react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||||
|
|
||||||
const Pagination = ({ total }: {
|
const Pagination = ({ total }: {
|
||||||
total: number,
|
total: number,
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
import { fetchNocturneApi } from '@/app/blog/api';
|
||||||
import BlogPostCard from '@/app/(pages)/blog/BlogPostCard';
|
import { MotionDiv } from '@/app/public/MotionDiv';
|
||||||
|
import BlogPostCard from '@/app/blog/BlogPostCard';
|
||||||
import { Card, CardBody } from '@nextui-org/card';
|
import { Card, CardBody } from '@nextui-org/card';
|
||||||
|
|
||||||
import * as motion from 'motion/react-client';
|
|
||||||
|
|
||||||
const PostList = async ({ page }: { page: number }) => {
|
const PostList = async ({ page }: { page: number }) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`);
|
const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`);
|
||||||
@@ -17,13 +16,13 @@ const PostList = async ({ page }: { page: number }) => {
|
|||||||
<ul className={ 'flex flex-col items-center w-full space-y-7' }>
|
<ul className={ 'flex flex-col items-center w-full space-y-7' }>
|
||||||
{ posts.map((post: JSON, key: number) => (
|
{ posts.map((post: JSON, key: number) => (
|
||||||
<li key={ key } className={ 'w-full' }>
|
<li key={ key } className={ 'w-full' }>
|
||||||
<motion.div
|
<MotionDiv
|
||||||
initial={ { y: 20, opacity: 0 } }
|
initial={ { y: 20, opacity: 0 } }
|
||||||
animate={ { y: 0, opacity: 1 } }
|
animate={ { y: 0, opacity: 1 } }
|
||||||
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
|
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
|
||||||
>
|
>
|
||||||
<BlogPostCard post={ post } />
|
<BlogPostCard post={ post } />
|
||||||
</motion.div>
|
</MotionDiv>
|
||||||
</li>)
|
</li>)
|
||||||
) }
|
) }
|
||||||
</ul>
|
</ul>
|
||||||
File renamed without changes.
@@ -1,10 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { SITE_CONFIG } from '@/src/constants';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: `Blog - ${ SITE_CONFIG.name }`
|
|
||||||
};
|
|
||||||
|
|
||||||
function Layout({
|
function Layout({
|
||||||
children
|
children
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import PostList from '@/app/(pages)/blog/PostList';
|
import PostList from '@/app/blog/PostList';
|
||||||
import Pagination from '@/app/(pages)/blog/Pagination';
|
import Pagination from '@/app/blog/Pagination';
|
||||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
import { fetchNocturneApi } from '@/app/blog/api';
|
||||||
|
|
||||||
async function Blog({
|
async function Blog({
|
||||||
searchParams
|
searchParams
|
||||||
File renamed without changes.
+13
-21
@@ -1,32 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MinifyPostCard from '@/app/(pages)/blog/post/[id]/MinifyPostCard';
|
import MinifyPostCard from '@/app/blog/post/[id]/MinifyPostCard';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FaCreativeCommons } from 'react-icons/fa6';
|
import { FaCreativeCommons } from 'react-icons/fa6';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// post: any
|
post: any
|
||||||
post_id: string;
|
|
||||||
adjacent_posts: any
|
|
||||||
perm_link: string | undefined;
|
|
||||||
author: string | undefined;
|
|
||||||
author_link: string | undefined;
|
|
||||||
first_update: string;
|
|
||||||
last_update: string | undefined;
|
|
||||||
cc: string | undefined;
|
|
||||||
title: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Footer = (props: Props) => {
|
const Footer = (props: Props) => {
|
||||||
const { adjacent_posts, perm_link, post_id, first_update, last_update, title } = props;
|
const { post } = props;
|
||||||
|
|
||||||
const prev_post = adjacent_posts.prev;
|
const adjacent_posts = post['adj'];
|
||||||
const next_post = adjacent_posts.next;
|
const prev_post = adjacent_posts['prev'];
|
||||||
const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`;
|
const next_post = adjacent_posts['next'];
|
||||||
const author = props.author ?? 'Tao';
|
const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`;
|
||||||
const author_link = props.author_link ?? '/about/me';
|
const author = post['meta']['author'] ?? 'Tao';
|
||||||
const post_date = new Date(first_update);
|
const author_link = post['meta']['author_link'] ?? '/about';
|
||||||
const update_date: Date | undefined = last_update ? new Date(last_update) : undefined;
|
const post_date = new Date(post['first_update']);
|
||||||
const cc = props.cc ?? 'by-nc-sa';
|
const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined;
|
||||||
|
const cc = post['meta']['cc'] ?? 'by-nc-sa';
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +35,7 @@ const Footer = (props: Props) => {
|
|||||||
|
|
||||||
<div className={ 'flex flex-col space-y-4 relative' }>
|
<div className={ 'flex flex-col space-y-4 relative' }>
|
||||||
<div className={ 'z-[2]' }>
|
<div className={ 'z-[2]' }>
|
||||||
<p className={ 'text-xl font-bold' }>{ title }</p>
|
<p className={ 'text-xl font-bold' }>{ post['meta']['title'] }</p>
|
||||||
<Link href={ link }>
|
<Link href={ link }>
|
||||||
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
|
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
|
||||||
</Link>
|
</Link>
|
||||||
+23
-23
@@ -3,57 +3,57 @@ import Image from 'next/image';
|
|||||||
import { Chip } from '@nextui-org/react';
|
import { Chip } from '@nextui-org/react';
|
||||||
import { MdCalendarMonth } from 'react-icons/md';
|
import { MdCalendarMonth } from 'react-icons/md';
|
||||||
|
|
||||||
// type Props = {
|
|
||||||
// post: any
|
|
||||||
// };
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
post: any
|
||||||
subtitle: string;
|
};
|
||||||
summary: string | undefined;
|
|
||||||
category: string;
|
|
||||||
tags: string[];
|
|
||||||
first_update: string;
|
|
||||||
header_img: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const Headers = (post: Props) => {
|
const Headers = (props: Props) => {
|
||||||
|
let post = props.post;
|
||||||
|
let title = post['title'];
|
||||||
|
let sub_title = post['sub_title'];
|
||||||
|
// let summary = (post as any)['summary'];
|
||||||
|
let category = post['category'];
|
||||||
|
let tags = post['tags'];
|
||||||
|
// let last_update = new Date((post as any)['last_update']);
|
||||||
|
let first_update = new Date(post['first_update']);
|
||||||
|
// let post_id = (post as any)['post_id'];
|
||||||
|
let header_img = post['meta']['header_img'] as string | null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ post.header_img && <div className={ `w-full overflow h-96 relative` }>
|
{ header_img && <div className={ `w-full overflow h-96 relative` }>
|
||||||
<Image
|
<Image
|
||||||
alt={ 'cover picture' }
|
alt={ 'cover picture' }
|
||||||
src={ post.header_img }
|
src={ header_img }
|
||||||
className={ `w-full object-cover rounded-b-none z-[5]` }
|
className={ `w-full object-cover rounded-b-none z-[5]` }
|
||||||
fill
|
fill
|
||||||
></Image>
|
></Image>
|
||||||
<Image
|
<Image
|
||||||
alt={ 'cover picture' }
|
alt={ 'cover picture' }
|
||||||
src={ post.header_img }
|
src={ header_img }
|
||||||
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
|
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
|
||||||
fill
|
fill
|
||||||
></Image>
|
></Image>
|
||||||
</div> }
|
</div> }
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={ post.header_img ?
|
className={ header_img ?
|
||||||
'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30'
|
'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30'
|
||||||
: 'mx-6 mt-6'
|
: 'mx-6 mt-6'
|
||||||
}>
|
}>
|
||||||
<div className={ post.header_img ? `p-4` : 'p-4 pt-2' }>
|
<div className={ header_img ? `p-4` : 'p-4 pt-2' }>
|
||||||
<Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
|
<Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
|
||||||
variant={ `solid` }>{ post.category }</Chip>
|
variant={ `solid` }>{ category }</Chip>
|
||||||
<p className={ `text-4xl font-bold` }> { post.title }</p>
|
<p className={ `text-4xl font-bold` }> { title }</p>
|
||||||
<p className={ `text-xl` }> { post.subtitle }</p>
|
<p className={ `text-xl` }> { sub_title }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }>
|
<div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }>
|
||||||
<div>
|
<div>
|
||||||
<ul className={ `flex flex-row flex-wrap` }>
|
<ul className={ `flex flex-row flex-wrap` }>
|
||||||
{
|
{
|
||||||
post.tags.map((tag: string, key: any) => (
|
tags.map((tag: string, key: any) => (
|
||||||
<li key={ key } className={ `mx-1 mb-1` }>
|
<li key={ key } className={ `mx-1 mb-1` }>
|
||||||
<Chip>{ tag }</Chip>
|
<Chip>{ tag }</Chip>
|
||||||
</li>
|
</li>
|
||||||
@@ -64,7 +64,7 @@ const Headers = (post: Props) => {
|
|||||||
<div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }>
|
<div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }>
|
||||||
<MdCalendarMonth />
|
<MdCalendarMonth />
|
||||||
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
|
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
|
||||||
{ (new Date(post.first_update)).toLocaleString('default', {
|
{ first_update.toLocaleString('default', {
|
||||||
month: `long`,
|
month: `long`,
|
||||||
day: `numeric`,
|
day: `numeric`,
|
||||||
year: `numeric`
|
year: `numeric`
|
||||||
+4
-4
@@ -43,10 +43,10 @@ const MinifyPostCard = (props: Props) => {
|
|||||||
}
|
}
|
||||||
{ !img_url &&
|
{ !img_url &&
|
||||||
<div className={ 'h-full grid p-6 place-content-center' }>
|
<div className={ 'h-full grid p-6 place-content-center' }>
|
||||||
<div className={ 'flex flex-col max-h-fit text-left' }>
|
<div className={ 'flex flex-row items-center text-center max-h-fit' }>
|
||||||
<p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p>
|
<p className={ 'text-3xl font-bold' }>{ post['title'] }</p>
|
||||||
<div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div>
|
<div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div>
|
||||||
<p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p>
|
<p className={ 'text-xl' }>{ post['sub_title'] }</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardBody } from '@nextui-org/card';
|
||||||
|
import Headers from '@/app/blog/post/[id]/Headers';
|
||||||
|
import Footer from '@/app/blog/post/[id]/Footer';
|
||||||
|
import { fetchNocturneApi } from '@/app/blog/api';
|
||||||
|
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function PostCard(props: Props) {
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchNocturneApi(`/blog/get_post/${ props.id }`);
|
||||||
|
const ret: any = await res.json();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card className={ `max-w-full relative` }>
|
||||||
|
|
||||||
|
{/* card headers */ }
|
||||||
|
<Headers post={ ret } />
|
||||||
|
|
||||||
|
{/* card body */ }
|
||||||
|
<CardBody>
|
||||||
|
<div className={ 'p-5' }>
|
||||||
|
<MarkdownRenderer>
|
||||||
|
{ ret['content'] }
|
||||||
|
</MarkdownRenderer>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
|
||||||
|
|
||||||
|
{/* card footer */ }
|
||||||
|
<Footer post={ ret } />
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
return <div>
|
||||||
|
<Card className={ `w-full` }>
|
||||||
|
<CardBody>
|
||||||
|
Fatal: { e.toString() }
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostCard;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PostCard from '@/app/blog/post/[id]/PostCard';
|
||||||
|
import { MotionDiv } from '@/app/public/MotionDiv';
|
||||||
|
import ButtonGoBack from '@/app/blog/post/[id]/ButtonGoBack';
|
||||||
|
|
||||||
|
async function BlogReader({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionDiv
|
||||||
|
className={ `max-w-full` }
|
||||||
|
initial={ { y: 20, opacity: 0 } }
|
||||||
|
animate={ { y: 0, opacity: 1 } }
|
||||||
|
transition={ { ease: 'easeInOut', duration: 0.5 } }
|
||||||
|
>
|
||||||
|
<div className={ `max-w-full` }>
|
||||||
|
<PostCard id={ (await params).id } />
|
||||||
|
</div>
|
||||||
|
<div className={ 'mt-3' }>
|
||||||
|
<ButtonGoBack />
|
||||||
|
</div>
|
||||||
|
</MotionDiv>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogReader;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
IoFlask,
|
|
||||||
IoHome,
|
|
||||||
IoInformationCircle,
|
|
||||||
IoLink,
|
|
||||||
IoLogoGithub,
|
|
||||||
IoMail,
|
|
||||||
IoPencil,
|
|
||||||
IoSearch
|
|
||||||
} from 'react-icons/io5';
|
|
||||||
|
|
||||||
export const entries: { display_name: string; href: string; icon: React.ReactNode }[] = [
|
|
||||||
{
|
|
||||||
icon: <IoHome size={ `20px` }></IoHome>,
|
|
||||||
display_name: 'Home',
|
|
||||||
href: '/'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IoInformationCircle size={ `20px` }></IoInformationCircle>,
|
|
||||||
display_name: 'About',
|
|
||||||
href: '/about'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IoPencil size={ `20px` }></IoPencil>,
|
|
||||||
display_name: 'Blog',
|
|
||||||
href: '/blog'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IoLink size={ `20px` }></IoLink>,
|
|
||||||
display_name: 'Friends',
|
|
||||||
href: '/friends'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IoSearch size={ `20px` }></IoSearch>,
|
|
||||||
display_name: 'Search',
|
|
||||||
href: '/search'
|
|
||||||
}, {
|
|
||||||
icon: <IoFlask size={ '20px' } />,
|
|
||||||
display_name: 'Lab',
|
|
||||||
href: 'https://lab.lmt.moe'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const connections: { label: string, href: string, icon: React.ReactNode }[] = [
|
|
||||||
{
|
|
||||||
label: 'Github',
|
|
||||||
href: 'https://github.com/Lumither',
|
|
||||||
icon: <IoLogoGithub size={ `30px` } />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'email',
|
|
||||||
href: 'mailto:lumither@outlook.com',
|
|
||||||
icon: <IoMail size={ `30px` } />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { SITE_CONFIG } from '@/src/constants';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: `Friends - ${ SITE_CONFIG.name }`
|
|
||||||
};
|
|
||||||
|
|
||||||
function Layout({
|
function Layout({
|
||||||
children
|
children
|
||||||
File renamed without changes.
+12
-7
@@ -2,14 +2,13 @@ import type { Metadata } from 'next';
|
|||||||
import './globals.css';
|
import './globals.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import { SITE_CONFIG } from '@/src/constants';
|
import Navbar from '@/components/Navbar';
|
||||||
|
import Footer from '@/components/Footer';
|
||||||
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
openGraph: {
|
title: 'Lumitherの酒馆',
|
||||||
siteName: SITE_CONFIG.name
|
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...'
|
||||||
},
|
|
||||||
title: SITE_CONFIG.name,
|
|
||||||
description: SITE_CONFIG.desc
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -21,7 +20,13 @@ export default function RootLayout({
|
|||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }>
|
<body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }>
|
||||||
<NextThemesProvider attribute="class" defaultTheme="system">
|
<NextThemesProvider attribute="class" defaultTheme="system">
|
||||||
{ children }
|
<div className={ `justify-center flex flex-row w-full max-w-full` }>
|
||||||
|
<Navbar />
|
||||||
|
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
|
||||||
|
{ children }
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</NextThemesProvider>
|
</NextThemesProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-44
@@ -1,51 +1,10 @@
|
|||||||
'use client';
|
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
|
||||||
import { Button } from '@nextui-org/react';
|
|
||||||
import { entries } from '@/app/entries';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useLocalStorage } from 'react-use';
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|
||||||
const isMobile = useScreenSizeTrigger('lg');
|
|
||||||
|
|
||||||
const [ test, setTest, removeTest ] = useLocalStorage('test', 'test_value');
|
|
||||||
console.log(test);
|
|
||||||
// setTest('test value 2');
|
|
||||||
// console.log(test);
|
|
||||||
// removeTest();
|
|
||||||
// console.log(test);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ `h-dvh relative` }>
|
<div className={ `h-dvh` }>
|
||||||
<ul className={ `flex flex-col space-y-2 w-full` }>
|
<ConstructionPlaceHolder />
|
||||||
{
|
|
||||||
entries.map((meta, key) => (
|
|
||||||
<li key={ key }>
|
|
||||||
<Button
|
|
||||||
as={ Link }
|
|
||||||
variant={ `light` }
|
|
||||||
color={ `default` }
|
|
||||||
fullWidth
|
|
||||||
isIconOnly={ isMobile }
|
|
||||||
className={ 'flex lg:justify-start w-fit lg:w-full' }
|
|
||||||
aria-label={ `navbar: ${ meta.display_name }` }
|
|
||||||
href={ meta.href }
|
|
||||||
>
|
|
||||||
{ meta.icon }
|
|
||||||
<p className={ `font-bold hidden lg:block` }>{ meta.display_name }</p>
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<div
|
|
||||||
className={ 'absolute bottom-0 w-full h-60 bg-yellow-500 p-12' }
|
|
||||||
>
|
|
||||||
<div className={ 'bg-blue-500 h-full w-full rounded-3xl backdrop-blur' }></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const MotionDiv = motion.div;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -3,12 +3,65 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button } from '@nextui-org/react';
|
import { Button } from '@nextui-org/react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import {
|
||||||
|
IoFlask,
|
||||||
|
IoHome,
|
||||||
|
IoInformationCircle,
|
||||||
|
IoLink,
|
||||||
|
IoLogoGithub,
|
||||||
|
IoMail,
|
||||||
|
IoPencil,
|
||||||
|
IoSearch
|
||||||
|
} from 'react-icons/io5';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import ThemeSwitcher from '@/components/ThemeSwitcher';
|
import ThemeSwitcher from '@/components/ThemeSwitcher';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||||
import { connections, entries } from '@/app/entries';
|
|
||||||
|
|
||||||
|
const entries: { display_name: string; href: string; icon: React.ReactNode }[] = [
|
||||||
|
{
|
||||||
|
icon: <IoHome size={ `20px` }></IoHome>,
|
||||||
|
display_name: 'Home',
|
||||||
|
href: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IoInformationCircle size={ `20px` }></IoInformationCircle>,
|
||||||
|
display_name: 'About',
|
||||||
|
href: '/about'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IoPencil size={ `20px` }></IoPencil>,
|
||||||
|
display_name: 'Blog',
|
||||||
|
href: '/blog'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IoLink size={ `20px` }></IoLink>,
|
||||||
|
display_name: 'Friends',
|
||||||
|
href: '/friends'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IoSearch size={ `20px` }></IoSearch>,
|
||||||
|
display_name: 'Search',
|
||||||
|
href: '/search'
|
||||||
|
}, {
|
||||||
|
icon: <IoFlask size={ '20px' } />,
|
||||||
|
display_name: 'Lab',
|
||||||
|
href: 'https://lab.lmt.moe'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const connections: { label: string, href: string, icon: React.ReactNode }[] = [
|
||||||
|
{
|
||||||
|
label: 'Github',
|
||||||
|
href: 'https://github.com/Lumither',
|
||||||
|
icon: <IoLogoGithub size={ `30px` } />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'email',
|
||||||
|
href: 'mailto:lumither@outlook.com',
|
||||||
|
icon: <IoMail size={ `30px` } />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
function Navbar() {
|
function Navbar() {
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
|
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
|
||||||
import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md';
|
import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md';
|
||||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||||
|
|
||||||
const switcherElements = new Map(
|
const switcherElements = new Map(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nextui-org/react": "^2.4.6",
|
"@nextui-org/react": "^2.4.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
"framer-motion": "^11.3.24",
|
||||||
"lxgw-wenkai-webfont": "^1.7.0",
|
"lxgw-wenkai-webfont": "^1.7.0",
|
||||||
"motion": "^12.3.1",
|
|
||||||
"next": "^15.0.3",
|
"next": "^15.0.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
"react-icons": "^5.2.1",
|
"react-icons": "^5.2.1",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-use": "^17.6.0",
|
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
|
|
||||||
export abstract class Activity {
|
|
||||||
protected constructor(
|
|
||||||
readonly time: Date
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract toElement(): React.ReactElement;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class GhActivity extends Activity {
|
|
||||||
protected constructor(time: Date) {
|
|
||||||
super(time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const GH_USER_NAME = 'Lumither';
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { GhActivity } from '@/src/HomePageActivities/activity';
|
|
||||||
import { Card, CardBody } from '@nextui-org/card';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export class StarRepo extends GhActivity {
|
|
||||||
constructor(
|
|
||||||
readonly repoName: string,
|
|
||||||
readonly repoUrl: string,
|
|
||||||
time: Date
|
|
||||||
) {
|
|
||||||
super(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
toElement() {
|
|
||||||
return (
|
|
||||||
// <div>
|
|
||||||
// <Link
|
|
||||||
// href={ this.repoUrl }
|
|
||||||
// >{ `${ GH_USER_NAME } started ${ this.repoName } at ${ this.time }` }</Link>
|
|
||||||
// </div>
|
|
||||||
<Card
|
|
||||||
isHoverable={ true }
|
|
||||||
as={ Link }
|
|
||||||
href={ this.repoUrl }
|
|
||||||
className={ 'bg-black/20' }
|
|
||||||
>
|
|
||||||
<CardBody>
|
|
||||||
<p className={ 'text-sm' }>{ `Stared GitHub repository '${ this.repoName }'` }</p>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { GhActivity } from '@/src/HomePageActivities/activity';
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
import { StarRepo } from '@/src/HomePageActivities/ghActivities/starRepo';
|
|
||||||
import { GH_USER_NAME } from '@/src/HomePageActivities/constants';
|
|
||||||
|
|
||||||
|
|
||||||
export const getGhActivities: () => Promise<GhActivity[]> = async () => {
|
|
||||||
const data = await fetch(`https://api.github.com/users/${ GH_USER_NAME }/events`, {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/vnd.github+json',
|
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data
|
|
||||||
.json()
|
|
||||||
.then((ret: any): GhActivity[] => (
|
|
||||||
ret
|
|
||||||
.map((res: any) => (
|
|
||||||
match(res)
|
|
||||||
.returnType<GhActivity | undefined>()
|
|
||||||
// star repo
|
|
||||||
.with(
|
|
||||||
{ type: 'WatchEvent', payload: { action: 'started' } }
|
|
||||||
, () => (
|
|
||||||
new StarRepo(res.repo.name, res.repo.url, res.created_at)
|
|
||||||
))
|
|
||||||
// to be continued
|
|
||||||
.otherwise(() => undefined))
|
|
||||||
)
|
|
||||||
.filter((res: GhActivity | undefined) => res !== undefined)
|
|
||||||
));
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export const SITE_CONFIG = {
|
|
||||||
name: 'Atelier Lumiaethor',
|
|
||||||
desc: ''
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
target/
|
|
||||||
|
|
||||||
Cargo.lock
|
|
||||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user