10 Commits
109 changed files with 1293 additions and 1034 deletions

No files matched your search

+10 -28
View File
@@ -1,34 +1,16 @@
# `Nocturne: personal website & CMS` # `Nocturne: Personal Website`
> This is the last working version This is the code repo of my [personal website](https://lumither.com),
and [here](https://github.com/Lumither/blog-posts/) is the link to the
This is the code repo of my personal website, and [here](https://github.com/Lumither/blog-posts/) is the link of the content (blog posts etc.) repo.
content repo.
Powered by Next.js and Rust, built with passion and love Powered by Next.js and Rust, built with passion and love
*will add more here in the future > [!NOTE]
> This system is not production ready, and contains bugs, (security risk potentially).
> But have fun on it =)
## Nav: # Todos
<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...
+2 -3
View File
@@ -1,8 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "3"
members = [ members = [
"api_server", "server",
"constants",
"macros", "macros",
"markdown", "markdown",
] ]
-25
View File
@@ -1,25 +0,0 @@
[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"
@@ -1,26 +0,0 @@
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()})))
}
}
}
-153
View File
@@ -1,153 +0,0 @@
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)))
}
-3
View File
@@ -1,3 +0,0 @@
pub mod get_page_count;
pub mod get_post;
pub mod get_post_list;
-14
View File
@@ -1,14 +0,0 @@
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(),
]
}
-17
View File
@@ -1,17 +0,0 @@
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))
}
-12
View File
@@ -1,12 +0,0 @@
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()
}
-49
View File
@@ -1,49 +0,0 @@
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");
}
-52
View File
@@ -1,52 +0,0 @@
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(())
}
-13
View File
@@ -1,13 +0,0 @@
mod blog;
mod init;
mod scheduler;
mod utils;
#[tokio::main]
async fn main() {
init::load_env();
let _guards = init::logger::init();
init::start().await;
}
-194
View File
@@ -1,194 +0,0 @@
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(())
}
}
@@ -1,3 +0,0 @@
pub trait TaskFunc: Fn() + Send + Sync + 'static {}
impl<T> TaskFunc for T where T: Fn() + Send + Sync + Clone + 'static {}
@@ -1,8 +0,0 @@
use chrono::{DateTime, Utc};
pub mod basic;
pub trait Task: Send + Sync {
fn get_next_execution(&self) -> Option<DateTime<Utc>>;
fn call(&self);
}
+1 -1
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "macros" name = "macros"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
tracing = "0.1.40" tracing = "0.1.40"
+6
View File
@@ -0,0 +1,6 @@
#[macro_export]
macro_rules! dev_consume {
($($var:ident),+) => {
$(let _ = $var;)*
};
}
+24
View File
@@ -0,0 +1,24 @@
/// 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))
};
}
+2
View File
@@ -1 +1,3 @@
mod dev;
mod include_str;
mod log; mod log;
+21 -2
View File
@@ -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,6 +18,25 @@ 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]
@@ -25,7 +44,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::{fmt, Registry}; use tracing_subscriber::{Registry, fmt};
let format = tracing_subscriber::fmt::format() let format = tracing_subscriber::fmt::format()
.with_level(true) .with_level(true)
+2 -2
View File
@@ -1,14 +1,14 @@
[package] [package]
name = "markdown" name = "markdown"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[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.9.0" yaml-rust2 = "0.10.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"
+1 -1
View File
@@ -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::general_purpose, Engine}; use base64::{Engine, engine::general_purpose};
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
+29
View File
@@ -0,0 +1,29 @@
[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"
+16
View File
@@ -0,0 +1,16 @@
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);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,7 +2,8 @@ use std::env;
use std::error::Error; use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
use constants::config::database::{default_value, var_name}; use crate::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;
@@ -12,7 +13,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: temporal solution, to be fixed .max_connections(1) // todo: temporary solution, to be fixed
.connect_with(db_connect_option) .connect_with(db_connect_option)
.await?) .await?)
} }
@@ -1,14 +1,15 @@
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::{
filter::LevelFilter, fmt, fmt::format::Writer, fmt::time::FormatTime, fmt::MakeWriter, Layer, Registry, filter::LevelFilter, fmt, fmt::MakeWriter, fmt::format::Writer,
layer::SubscriberExt, Layer, Registry, fmt::time::FormatTime, layer::SubscriberExt,
}; };
struct LocalTimer; struct LocalTimer;
@@ -18,7 +19,7 @@ impl FormatTime for LocalTimer {
write!( write!(
w, w,
"{}", "{}",
Local::now().format(constants::time::DEFAULT_TIME_FORMAT) Local::now().format(crate::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,25 +34,17 @@ pub async fn start() {
pool pool
} }
Err(e) => { Err(e) => {
panic_with_log!( error_panic!("database connection pool failed to initialize: {}", e);
Level::ERROR,
"database connection pool failed to initialize: {}",
e
);
} }
}; };
// schema init // // schema init
schema::init(&db_pool).await; // schema::init(&db_pool).await;
// load cron task ModuleTree::new()
cron::start(db_pool.clone()); .add_module(Blog {
db_handler: db_pool,
// api serve })
match server::start(db_pool).await { .blocking_serve()
Ok(_) => {} .await
Err(e) => {
panic_with_log!(Level::ERROR, "server panicked: {}", e)
}
}
} }
+23
View File
@@ -0,0 +1,23 @@
#[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;
}
@@ -0,0 +1,25 @@
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()})))
}
}
}
@@ -0,0 +1,2 @@
SELECT cast(ceil(count(*) / cast($1 AS FLOAT)) AS INTEGER) AS count
FROM posts;
Whitespace-only changes.
@@ -0,0 +1,34 @@
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 +1,6 @@
use std::error::Error; use std::error::Error;
use constants::blog::PAGE_SIZE; use crate::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,6 @@
pub mod get_page_count;
// todo: remove exception tag
#[allow(dead_code)]
pub mod get_post;
// pub mod get_post_list;
@@ -1,8 +1,10 @@
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;
use crate::blog::cron::check_update::utils::get_md_file_basename; use crate::{
use crate::blog::cron::check_update::{error::PostIdxError, utils::parse_date}; modules::blog::cron::check_update::utils::get_md_file_basename,
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};
@@ -1,26 +1,28 @@
use std::env; use std::{
use std::fs::remove_dir_all; env,
use std::path::{Path, PathBuf}; fs::remove_dir_all,
path::{Path, PathBuf},
};
use crate::{ use crate::{
blog::cron::check_update::{ constants::config::general::{default_value, var_name},
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::{Pool, Postgres}; use sqlx::{PgPool, 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: &Pool<Postgres>) { pub fn task(db_connection: &PgPool) {
// 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) {
@@ -1,16 +1,18 @@
use dirs::home_dir;
use std::path::PathBuf; use std::path::PathBuf;
use crate::blog::cron::check_update::index::index_file; use crate::{
use chrono::{DateTime, FixedOffset, ParseResult}; constants::{
use constants::config::general::{default_value, var_name}; config::general::{default_value, var_name},
use constants::time::DEFAULT_DATE_FORMAT; time::DEFAULT_DATE_FORMAT,
},
modules::blog::cron::check_update::index::index_file,
};
use futures::executor::block_on; use chrono::{DateTime, FixedOffset, ParseResult};
use futures::future::join_all; use dirs::home_dir;
use futures::{executor::block_on, future::join_all};
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use tokio::runtime::Runtime; use tokio::{runtime::Runtime, spawn};
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 @@
// pub mod check_update;
+55
View File
@@ -0,0 +1,55 @@
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(),
]
}
}
@@ -0,0 +1,41 @@
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)
);
+81
View File
@@ -0,0 +1,81 @@
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());
});
}
}
+127
View File
@@ -0,0 +1,127 @@
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(())
}
}
+152
View File
@@ -0,0 +1,152 @@
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,8 +1,12 @@
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison; use std::{
use std::error::Error; error::Error,
use std::fmt::{Debug, Display, Formatter}; fmt::{Debug, Display, Formatter},
use std::sync::PoisonError; sync::PoisonError,
};
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison;
// todo: refactor with thiserror
pub enum SchedulerError { pub enum SchedulerError {
IdNotFound, IdNotFound,
IdAlreadyRunning, IdAlreadyRunning,
@@ -1,7 +1,10 @@
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 {
+15
View File
@@ -0,0 +1,15 @@
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
{
}
@@ -0,0 +1,37 @@
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,9 +1,9 @@
use crate::scheduler::task_func::TaskFunc; use std::{error::Error, pin::Pin, str::FromStr};
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 Task> { pub fn to_task(self) -> Box<dyn CronTask> {
Box::new(self) Box::new(self)
} }
} }
impl Task for BasicTask { impl CronTask 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) { fn call(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
(self.func)() Box::pin(async { (self.func)() })
} }
} }
+11
View File
@@ -0,0 +1,11 @@
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 + '_>>;
}
@@ -3,12 +3,11 @@
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837 /// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
/// ///
use git2::{ use git2::{
build, AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository, AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository, build,
}; };
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>,
@@ -16,6 +15,7 @@ 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,8 +23,6 @@ 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)?;
@@ -32,6 +30,7 @@ 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,
@@ -45,16 +44,11 @@ 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( repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?;
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,
@@ -73,13 +67,12 @@ 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,
@@ -88,34 +81,26 @@ 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(),
@@ -132,17 +117,12 @@ 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,
@@ -156,23 +136,12 @@ 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 // Continue true
}, },
None, None,
None, None,
@@ -184,6 +153,7 @@ 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,4 +1,10 @@
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/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/(pages)/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/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch'; import { fetchText } from '@/app/fetch';
const Website = async () => { const Website = async () => {
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/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const Pagination = ({ total }: { const Pagination = ({ total }: {
total: number, total: number,
@@ -1,11 +1,12 @@
'use server'; 'use server';
import React from 'react'; import React from 'react';
import { fetchNocturneApi } from '@/app/blog/api'; import { fetchNocturneApi } from '@/app/(pages)/blog/api';
import { MotionDiv } from '@/app/public/MotionDiv'; import BlogPostCard from '@/app/(pages)/blog/BlogPostCard';
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 }`);
@@ -16,13 +17,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' }>
<MotionDiv <motion.div
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 } />
</MotionDiv> </motion.div>
</li>) </li>)
) } ) }
</ul> </ul>
File renamed without changes.
@@ -1,4 +1,10 @@
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/blog/PostList'; import PostList from '@/app/(pages)/blog/PostList';
import Pagination from '@/app/blog/Pagination'; import Pagination from '@/app/(pages)/blog/Pagination';
import { fetchNocturneApi } from '@/app/blog/api'; import { fetchNocturneApi } from '@/app/(pages)/blog/api';
async function Blog({ async function Blog({
searchParams searchParams
@@ -1,24 +1,32 @@
import React from 'react'; import React from 'react';
import MinifyPostCard from '@/app/blog/post/[id]/MinifyPostCard'; import MinifyPostCard from '@/app/(pages)/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 { post } = props; const { adjacent_posts, perm_link, post_id, first_update, last_update, title } = props;
const adjacent_posts = post['adj']; const prev_post = adjacent_posts.prev;
const prev_post = adjacent_posts['prev']; const next_post = adjacent_posts.next;
const next_post = adjacent_posts['next']; const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`;
const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`; const author = props.author ?? 'Tao';
const author = post['meta']['author'] ?? 'Tao'; const author_link = props.author_link ?? '/about/me';
const author_link = post['meta']['author_link'] ?? '/about'; const post_date = new Date(first_update);
const post_date = new Date(post['first_update']); const update_date: Date | undefined = last_update ? new Date(last_update) : undefined;
const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined; const cc = props.cc ?? 'by-nc-sa';
const cc = post['meta']['cc'] ?? 'by-nc-sa';
return ( return (
@@ -35,7 +43,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' }>{ post['meta']['title'] }</p> <p className={ 'text-xl font-bold' }>{ 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>
@@ -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 = {
post: any title: string;
}; subtitle: string;
summary: string | undefined;
category: string;
tags: string[];
first_update: string;
header_img: string | undefined;
}
const Headers = (props: Props) => { const Headers = (post: 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 (
<> <>
{ header_img && <div className={ `w-full overflow h-96 relative` }> { post.header_img && <div className={ `w-full overflow h-96 relative` }>
<Image <Image
alt={ 'cover picture' } alt={ 'cover picture' }
src={ header_img } src={ post.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={ header_img } src={ post.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={ header_img ? className={ post.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={ header_img ? `p-4` : 'p-4 pt-2' }> <div className={ post.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` }>{ category }</Chip> variant={ `solid` }>{ post.category }</Chip>
<p className={ `text-4xl font-bold` }> { title }</p> <p className={ `text-4xl font-bold` }> { post.title }</p>
<p className={ `text-xl` }> { sub_title }</p> <p className={ `text-xl` }> { post.subtitle }</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` }>
{ {
tags.map((tag: string, key: any) => ( post.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 = (props: 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` }>
{ first_update.toLocaleString('default', { { (new Date(post.first_update)).toLocaleString('default', {
month: `long`, month: `long`,
day: `numeric`, day: `numeric`,
year: `numeric` year: `numeric`
@@ -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-row items-center text-center max-h-fit' }> <div className={ 'flex flex-col max-h-fit text-left' }>
<p className={ 'text-3xl font-bold' }>{ post['title'] }</p> <p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p>
<div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div> <div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div>
<p className={ 'text-xl' }>{ post['sub_title'] }</p> <p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p>
</div> </div>
</div> </div>
} }
@@ -0,0 +1,55 @@
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;
@@ -0,0 +1,70 @@
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,4 +1,10 @@
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.
+30
View File
@@ -0,0 +1,30 @@
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;
Whitespace-only changes.
File renamed without changes.
+43
View File
@@ -0,0 +1,43 @@
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;
-53
View File
@@ -1,53 +0,0 @@
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;
-29
View File
@@ -1,29 +0,0 @@
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;
+56
View File
@@ -0,0 +1,56 @@
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` } />
}
];
+7 -12
View File
@@ -2,13 +2,14 @@ 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 Navbar from '@/components/Navbar'; import { SITE_CONFIG } from '@/src/constants';
import Footer from '@/components/Footer';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Lumitherの酒馆', openGraph: {
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...' siteName: SITE_CONFIG.name
},
title: SITE_CONFIG.name,
description: SITE_CONFIG.desc
}; };
export default function RootLayout({ export default function RootLayout({
@@ -20,13 +21,7 @@ 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">
<div className={ `justify-center flex flex-row w-full max-w-full` }> { children }
<Navbar />
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
{ children }
<Footer />
</div>
</div>
</NextThemesProvider> </NextThemesProvider>
</body> </body>
</html> </html>
+44 -3
View File
@@ -1,10 +1,51 @@
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder'; 'use client';
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` }> <div className={ `h-dvh relative` }>
<ConstructionPlaceHolder /> <ul className={ `flex flex-col space-y-2 w-full` }>
{
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>
); );
} }
-5
View File
@@ -1,5 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export const MotionDiv = motion.div;
+2 -55
View File
@@ -3,65 +3,12 @@
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/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/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() {
+1 -1
View File
@@ -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/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const switcherElements = new Map( const switcherElements = new Map(
[ [
+2 -1
View File
@@ -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,6 +20,7 @@
"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",
@@ -0,0 +1,18 @@
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);
}
}
@@ -0,0 +1 @@
export const GH_USER_NAME = 'Lumither';
@@ -0,0 +1,34 @@
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>
);
}
}
@@ -0,0 +1,32 @@
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)
));
};
+4
View File
@@ -0,0 +1,4 @@
export const SITE_CONFIG = {
name: 'Atelier Lumiaethor',
desc: ''
};
+3
View File
@@ -0,0 +1,3 @@
target/
Cargo.lock
@@ -1,5 +1,5 @@
[package] [package]
name = "constants" name = "gateway"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more