mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8358a8ae33 | ||
|
|
84ec754c65 | ||
|
|
bd90cd94d1 | ||
|
|
5da9ca3ae1 | ||
|
|
eef32ad695 | ||
|
|
acd0525513 | ||
|
|
8938c96409 |
No files matched your search
@@ -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, and [here](https://github.com/Lumither/blog-posts/) is the link of the
|
||||
content repo.
|
||||
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
|
||||
content (blog posts etc.) repo.
|
||||
|
||||
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:
|
||||
|
||||
<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
|
||||
# Todos
|
||||
|
||||
- [ ] the todo list lol
|
||||
- [ ] and much more...
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"api_server",
|
||||
"constants",
|
||||
"server",
|
||||
"macros",
|
||||
"markdown",
|
||||
]
|
||||
@@ -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()})))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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(),
|
||||
]
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
};
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
mod include_str;
|
||||
mod log;
|
||||
@@ -6,7 +6,7 @@
|
||||
/// use macros::panic_with_log;
|
||||
/// use tracing::Level;
|
||||
///
|
||||
/// panic_with_log!(Level::ERROR, "Test panicked: {}", 42);
|
||||
/// panic_with_log!(Level::ERROR, "test panicked: {}", 42);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
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)]
|
||||
mod tests {
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[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"
|
||||
@@ -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::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::{ConnectOptions, Pool, Postgres};
|
||||
use tracing::info;
|
||||
@@ -12,7 +13,7 @@ pub async fn init() -> Result<Pool<Postgres>, Box<dyn Error>> {
|
||||
.unwrap()
|
||||
.disable_statement_logging();
|
||||
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)
|
||||
.await?)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::path::Path;
|
||||
use std::{env, io};
|
||||
|
||||
use crate::constants::config::general::{default_value, var_name};
|
||||
|
||||
use chrono::Local;
|
||||
use constants::config::general::{default_value, var_name};
|
||||
use dirs::home_dir;
|
||||
use tracing::info;
|
||||
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
|
||||
@@ -18,7 +19,7 @@ impl FormatTime for LocalTimer {
|
||||
write!(
|
||||
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;
|
||||
pub mod logger;
|
||||
mod misc;
|
||||
mod schema;
|
||||
pub mod server;
|
||||
|
||||
use macros::panic_with_log;
|
||||
use std::{env, panic};
|
||||
use tracing::{info, Level};
|
||||
|
||||
use crate::modules::{blog::Blog, ModuleTree};
|
||||
use macros::error_panic;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
pub fn load_env() {
|
||||
if let Some(env_file) = env::args().nth(1) {
|
||||
@@ -34,25 +34,17 @@ pub async fn start() {
|
||||
pool
|
||||
}
|
||||
Err(e) => {
|
||||
panic_with_log!(
|
||||
Level::ERROR,
|
||||
"database connection pool failed to initialize: {}",
|
||||
e
|
||||
);
|
||||
error_panic!("database connection pool failed to initialize: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// schema init
|
||||
schema::init(&db_pool).await;
|
||||
// // schema init
|
||||
// schema::init(&db_pool).await;
|
||||
|
||||
// load cron task
|
||||
cron::start(db_pool.clone());
|
||||
|
||||
// api serve
|
||||
match server::start(db_pool).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
panic_with_log!(Level::ERROR, "server panicked: {}", e)
|
||||
}
|
||||
}
|
||||
ModuleTree::new()
|
||||
.add_module(Blog {
|
||||
db_handler: db_pool,
|
||||
})
|
||||
.blocking_serve()
|
||||
.await
|
||||
}
|
||||
@@ -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::{extract::State, Json};
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{query, PgPool, Row};
|
||||
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;
|
||||
File renamed without changes.
Whitespace-only changes.
@@ -0,0 +1,30 @@
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
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 {
|
||||
let post = 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) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn select_post_with_str_id(db: &PgPool, id: &str) {
|
||||
todo!()
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
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 chrono::{DateTime, Utc};
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod get_page_count;
|
||||
pub mod get_post;
|
||||
pub mod get_post_list;
|
||||
// pub mod get_post_list;
|
||||
File renamed without changes.
+4
-2
@@ -1,8 +1,10 @@
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::blog::cron::check_update::utils::get_md_file_basename;
|
||||
use crate::blog::cron::check_update::{error::PostIdxError, utils::parse_date};
|
||||
use crate::{
|
||||
modules::blog::cron::check_update::utils::get_md_file_basename,
|
||||
modules::blog::cron::check_update::{error::PostIdxError, utils::parse_date},
|
||||
};
|
||||
use markdown::MdFile;
|
||||
|
||||
use sqlx::{query, PgPool, Pool, Postgres, Row};
|
||||
+7
-5
@@ -1,15 +1,17 @@
|
||||
use std::env;
|
||||
use std::fs::remove_dir_all;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
env,
|
||||
fs::remove_dir_all,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
blog::cron::check_update::{
|
||||
constants::config::general::{default_value, var_name},
|
||||
modules::blog::cron::check_update::{
|
||||
index::drop_index,
|
||||
utils::{expand_path, get_md_file_basename, index_files, search_md},
|
||||
},
|
||||
utils::git,
|
||||
};
|
||||
use constants::config::general::{default_value, var_name};
|
||||
use macros::panic_with_log;
|
||||
|
||||
use git2::{Delta, Repository};
|
||||
+11
-9
@@ -1,16 +1,18 @@
|
||||
use dirs::home_dir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::blog::cron::check_update::index::index_file;
|
||||
use chrono::{DateTime, FixedOffset, ParseResult};
|
||||
use constants::config::general::{default_value, var_name};
|
||||
use constants::time::DEFAULT_DATE_FORMAT;
|
||||
use crate::{
|
||||
constants::{
|
||||
config::general::{default_value, var_name},
|
||||
time::DEFAULT_DATE_FORMAT,
|
||||
},
|
||||
modules::blog::cron::check_update::index::index_file,
|
||||
};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use futures::future::join_all;
|
||||
use chrono::{DateTime, FixedOffset, ParseResult};
|
||||
use dirs::home_dir;
|
||||
use futures::{executor::block_on, future::join_all};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::spawn;
|
||||
use tokio::{runtime::Runtime, spawn};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub fn search_md(entry: PathBuf) -> Vec<PathBuf> {
|
||||
@@ -0,0 +1 @@
|
||||
// pub mod check_update;
|
||||
@@ -0,0 +1,52 @@
|
||||
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::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(),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
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 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 tokio::net::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());
|
||||
}
|
||||
|
||||
axum::serve(listener, app).await.unwrap_or_else(|e| {
|
||||
error_panic!("failed to start axum: {}", e.to_string());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
use crate::{
|
||||
scheduler::scheduler_error::SchedulerError,
|
||||
scheduler::scheduler_error::SchedulerError::IdAlreadyRunning,
|
||||
scheduler::scheduler_error::SchedulerError::IdNotFound, scheduler::sleeper::SkippableSleeper,
|
||||
scheduler::tasks::Task,
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::RwLock;
|
||||
use std::{collections::HashMap, sync::Arc, thread};
|
||||
|
||||
use crate::scheduler::{
|
||||
scheduler_error::{
|
||||
SchedulerError, SchedulerError::IdAlreadyRunning, SchedulerError::IdNotFound,
|
||||
},
|
||||
sleeper::SkippableSleeper,
|
||||
tasks::CronTask,
|
||||
};
|
||||
|
||||
use tracing::log::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -22,7 +30,7 @@ struct TaskStatus {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Scheduler {
|
||||
tasks: Arc<RwLock<HashMap<Uuid, Arc<Box<dyn Task>>>>>,
|
||||
tasks: Arc<RwLock<HashMap<Uuid, Arc<Box<dyn CronTask>>>>>,
|
||||
running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>,
|
||||
}
|
||||
|
||||
@@ -41,7 +49,7 @@ impl Scheduler {
|
||||
fn run_task(
|
||||
running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>,
|
||||
id: Uuid,
|
||||
task: Arc<Box<dyn Task>>,
|
||||
task: Arc<Box<dyn CronTask>>,
|
||||
) -> Result<(), SchedulerError> {
|
||||
if running.read()?.get(&id).is_some() {
|
||||
return Err(IdAlreadyRunning);
|
||||
@@ -151,13 +159,13 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&self, task: Box<dyn Task>) -> Result<Uuid, SchedulerError> {
|
||||
pub fn insert(&self, task: Box<dyn CronTask>) -> 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> {
|
||||
pub fn insert_list(&self, tasks: Vec<Box<dyn CronTask>>) -> Result<Vec<Uuid>, SchedulerError> {
|
||||
tasks.into_iter().map(|t| self.insert(t)).collect()
|
||||
}
|
||||
|
||||
+8
-4
@@ -1,8 +1,12 @@
|
||||
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::sync::PoisonError;
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Debug, Display, Formatter},
|
||||
sync::PoisonError,
|
||||
};
|
||||
|
||||
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison;
|
||||
|
||||
// todo: refactor with thiserror
|
||||
pub enum SchedulerError {
|
||||
IdNotFound,
|
||||
IdAlreadyRunning,
|
||||
+6
-3
@@ -1,7 +1,10 @@
|
||||
use std::{
|
||||
sync::{Arc, Condvar, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SkippableSleeper {
|
||||
File renamed without changes.
+7
-6
@@ -1,10 +1,11 @@
|
||||
use crate::scheduler::task_func::TaskFunc;
|
||||
use crate::scheduler::tasks::Task;
|
||||
use chrono::{DateTime, Utc};
|
||||
use cron::Schedule;
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::scheduler::{task_func::TaskFunc, tasks::CronTask};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use cron::Schedule;
|
||||
|
||||
pub struct BasicTask {
|
||||
schedule: Schedule,
|
||||
func: Box<dyn TaskFunc>,
|
||||
@@ -21,12 +22,12 @@ impl BasicTask {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_task(self) -> Box<dyn Task> {
|
||||
pub fn to_task(self) -> Box<dyn CronTask> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for BasicTask {
|
||||
impl CronTask for BasicTask {
|
||||
fn get_next_execution(&self) -> Option<DateTime<Utc>> {
|
||||
self.schedule.upcoming(Utc).next()
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
||||
|
||||
pub mod basic;
|
||||
|
||||
pub trait Task: Send + Sync {
|
||||
pub trait CronTask: Send + Sync {
|
||||
fn get_next_execution(&self) -> Option<DateTime<Utc>>;
|
||||
fn call(&self);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ pub struct FileDelta {
|
||||
pub status: Delta,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn do_fetch<'a>(
|
||||
repo: &'a Repository,
|
||||
refs: &[&str],
|
||||
@@ -23,8 +24,6 @@ fn do_fetch<'a>(
|
||||
) -> Result<AnnotatedCommit<'a>, git2::Error> {
|
||||
let mut fetch_option = FetchOptions::new();
|
||||
|
||||
// Always fetch all tags.
|
||||
// Perform a download and also update tips
|
||||
fetch_option.download_tags(AutotagOption::All);
|
||||
remote.fetch(refs, Some(&mut fetch_option), None)?;
|
||||
|
||||
@@ -32,6 +31,7 @@ fn do_fetch<'a>(
|
||||
repo.reference_to_annotated_commit(&fetch_head)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn fast_forward(
|
||||
repo: &Repository,
|
||||
lb: &mut Reference,
|
||||
@@ -45,16 +45,11 @@ fn fast_forward(
|
||||
// println!("{}", msg);
|
||||
lb.set_target(rc.id(), &msg)?;
|
||||
repo.set_head(&name)?;
|
||||
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(),
|
||||
))?;
|
||||
repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn normal_merge(
|
||||
repo: &Repository,
|
||||
local: &AnnotatedCommit,
|
||||
@@ -73,13 +68,12 @@ fn normal_merge(
|
||||
return Ok(());
|
||||
}
|
||||
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 sig = repo.signature()?;
|
||||
let local_commit = repo.find_commit(local.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(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
@@ -88,34 +82,26 @@ fn normal_merge(
|
||||
&result_tree,
|
||||
&[&local_commit, &remote_commit],
|
||||
)?;
|
||||
// Set working tree to match head.
|
||||
repo.checkout_head(None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn do_merge<'a>(
|
||||
repo: &'a Repository,
|
||||
remote_branch: &str,
|
||||
fetch_commit: AnnotatedCommit<'a>,
|
||||
) -> Result<Vec<FileDelta>, git2::Error> {
|
||||
// 1. do a merge analysis
|
||||
let analysis = repo.merge_analysis(&[&fetch_commit])?;
|
||||
|
||||
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
|
||||
|
||||
// 2. Do the appropriate merge
|
||||
if analysis.0.is_fast_forward() {
|
||||
// println!("Doing a fast-forward");
|
||||
// do a fast-forward
|
||||
let ref_name = format!("refs/heads/{}", remote_branch);
|
||||
match repo.find_reference(&ref_name) {
|
||||
Ok(mut r) => {
|
||||
fast_forward(repo, &mut r, &fetch_commit)?;
|
||||
}
|
||||
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(
|
||||
&ref_name,
|
||||
fetch_commit.id(),
|
||||
@@ -132,17 +118,12 @@ fn do_merge<'a>(
|
||||
}
|
||||
};
|
||||
} else if analysis.0.is_normal() {
|
||||
// do a normal merge
|
||||
normal_merge(repo, &head_commit, &fetch_commit)?;
|
||||
}
|
||||
// else {
|
||||
// // already up-to-date
|
||||
// println!("Nothing to do...");
|
||||
// }
|
||||
|
||||
calculate_diff(repo, &head_commit, &fetch_commit)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn calculate_diff(
|
||||
repo: &Repository,
|
||||
local: &AnnotatedCommit,
|
||||
@@ -156,23 +137,12 @@ fn calculate_diff(
|
||||
|
||||
if let Err(e) = diff.foreach(
|
||||
&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 {
|
||||
old_path: d.old_file().path().map(PathBuf::from),
|
||||
new_path: d.new_file().path().map(PathBuf::from),
|
||||
status: d.status(),
|
||||
});
|
||||
true // Continue
|
||||
true
|
||||
},
|
||||
None,
|
||||
None,
|
||||
@@ -184,6 +154,7 @@ fn calculate_diff(
|
||||
Ok(updated_files.into_iter().collect())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn sync(
|
||||
remote_name: &str,
|
||||
remote_branch: &str,
|
||||
File renamed without changes.
@@ -1,4 +1,10 @@
|
||||
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({
|
||||
children
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
||||
import { fetchText } from '@/app/fetch';
|
||||
|
||||
const Me = async () => {
|
||||
File renamed without changes.
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
||||
import { fetchText } from '@/app/fetch';
|
||||
|
||||
const Website = async () => {
|
||||
File renamed without changes.
File renamed without changes.
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import { Pagination as NextUIPagination } from '@nextui-org/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
||||
|
||||
const Pagination = ({ total }: {
|
||||
total: number,
|
||||
@@ -1,11 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import React from 'react';
|
||||
import { fetchNocturneApi } from '@/app/blog/api';
|
||||
import { MotionDiv } from '@/app/public/MotionDiv';
|
||||
import BlogPostCard from '@/app/blog/BlogPostCard';
|
||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
||||
import BlogPostCard from '@/app/(pages)/blog/BlogPostCard';
|
||||
import { Card, CardBody } from '@nextui-org/card';
|
||||
|
||||
import * as motion from 'motion/react-client';
|
||||
|
||||
const PostList = async ({ page }: { page: number }) => {
|
||||
try {
|
||||
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' }>
|
||||
{ posts.map((post: JSON, key: number) => (
|
||||
<li key={ key } className={ 'w-full' }>
|
||||
<MotionDiv
|
||||
<motion.div
|
||||
initial={ { y: 20, opacity: 0 } }
|
||||
animate={ { y: 0, opacity: 1 } }
|
||||
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
|
||||
>
|
||||
<BlogPostCard post={ post } />
|
||||
</MotionDiv>
|
||||
</motion.div>
|
||||
</li>)
|
||||
) }
|
||||
</ul>
|
||||
File renamed without changes.
@@ -1,4 +1,10 @@
|
||||
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({
|
||||
children
|
||||
@@ -1,6 +1,6 @@
|
||||
import PostList from '@/app/blog/PostList';
|
||||
import Pagination from '@/app/blog/Pagination';
|
||||
import { fetchNocturneApi } from '@/app/blog/api';
|
||||
import PostList from '@/app/(pages)/blog/PostList';
|
||||
import Pagination from '@/app/(pages)/blog/Pagination';
|
||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
||||
|
||||
async function Blog({
|
||||
searchParams
|
||||
File renamed without changes.
+21
-13
@@ -1,24 +1,32 @@
|
||||
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 { FaCreativeCommons } from 'react-icons/fa6';
|
||||
|
||||
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 { 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 next_post = adjacent_posts['next'];
|
||||
const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`;
|
||||
const author = post['meta']['author'] ?? 'Tao';
|
||||
const author_link = post['meta']['author_link'] ?? '/about';
|
||||
const post_date = new Date(post['first_update']);
|
||||
const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined;
|
||||
const cc = post['meta']['cc'] ?? 'by-nc-sa';
|
||||
const prev_post = adjacent_posts.prev;
|
||||
const next_post = adjacent_posts.next;
|
||||
const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`;
|
||||
const author = props.author ?? 'Tao';
|
||||
const author_link = props.author_link ?? '/about/me';
|
||||
const post_date = new Date(first_update);
|
||||
const update_date: Date | undefined = last_update ? new Date(last_update) : undefined;
|
||||
const cc = props.cc ?? 'by-nc-sa';
|
||||
|
||||
|
||||
return (
|
||||
@@ -35,7 +43,7 @@ const Footer = (props: Props) => {
|
||||
|
||||
<div className={ 'flex flex-col space-y-4 relative' }>
|
||||
<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 }>
|
||||
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
|
||||
</Link>
|
||||
+23
-23
@@ -3,57 +3,57 @@ import Image from 'next/image';
|
||||
import { Chip } from '@nextui-org/react';
|
||||
import { MdCalendarMonth } from 'react-icons/md';
|
||||
|
||||
// type Props = {
|
||||
// post: any
|
||||
// };
|
||||
|
||||
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) => {
|
||||
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;
|
||||
const Headers = (post: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{ header_img && <div className={ `w-full overflow h-96 relative` }>
|
||||
{ post.header_img && <div className={ `w-full overflow h-96 relative` }>
|
||||
<Image
|
||||
alt={ 'cover picture' }
|
||||
src={ header_img }
|
||||
src={ post.header_img }
|
||||
className={ `w-full object-cover rounded-b-none z-[5]` }
|
||||
fill
|
||||
></Image>
|
||||
<Image
|
||||
alt={ 'cover picture' }
|
||||
src={ header_img }
|
||||
src={ post.header_img }
|
||||
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
|
||||
fill
|
||||
></Image>
|
||||
</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-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` }
|
||||
variant={ `solid` }>{ category }</Chip>
|
||||
<p className={ `text-4xl font-bold` }> { title }</p>
|
||||
<p className={ `text-xl` }> { sub_title }</p>
|
||||
variant={ `solid` }>{ post.category }</Chip>
|
||||
<p className={ `text-4xl font-bold` }> { post.title }</p>
|
||||
<p className={ `text-xl` }> { post.subtitle }</p>
|
||||
</div>
|
||||
|
||||
<div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }>
|
||||
<div>
|
||||
<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` }>
|
||||
<Chip>{ tag }</Chip>
|
||||
</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` }>
|
||||
<MdCalendarMonth />
|
||||
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
|
||||
{ first_update.toLocaleString('default', {
|
||||
{ (new Date(post.first_update)).toLocaleString('default', {
|
||||
month: `long`,
|
||||
day: `numeric`,
|
||||
year: `numeric`
|
||||
+4
-4
@@ -43,10 +43,10 @@ const MinifyPostCard = (props: Props) => {
|
||||
}
|
||||
{ !img_url &&
|
||||
<div className={ 'h-full grid p-6 place-content-center' }>
|
||||
<div className={ 'flex flex-row items-center text-center max-h-fit' }>
|
||||
<p className={ 'text-3xl font-bold' }>{ post['title'] }</p>
|
||||
<div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div>
|
||||
<p className={ 'text-xl' }>{ post['sub_title'] }</p>
|
||||
<div className={ 'flex flex-col max-h-fit text-left' }>
|
||||
<p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p>
|
||||
<div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div>
|
||||
<p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p>
|
||||
</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 { Metadata } from 'next';
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Friends - ${ SITE_CONFIG.name }`
|
||||
};
|
||||
|
||||
function Layout({
|
||||
children
|
||||
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
Whitespace-only changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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` } />
|
||||
}
|
||||
];
|
||||
+6
-11
@@ -2,13 +2,14 @@ import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
import React from 'react';
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Lumitherの酒馆',
|
||||
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...'
|
||||
openGraph: {
|
||||
siteName: SITE_CONFIG.name
|
||||
},
|
||||
title: SITE_CONFIG.name,
|
||||
description: SITE_CONFIG.desc
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -20,13 +21,7 @@ export default function RootLayout({
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }>
|
||||
<NextThemesProvider attribute="class" defaultTheme="system">
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+44
-3
@@ -1,10 +1,51 @@
|
||||
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
|
||||
'use client';
|
||||
|
||||
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() {
|
||||
|
||||
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 (
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
<div className={ `h-dvh relative` }>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const MotionDiv = motion.div;
|
||||
@@ -3,65 +3,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
IoFlask,
|
||||
IoHome,
|
||||
IoInformationCircle,
|
||||
IoLink,
|
||||
IoLogoGithub,
|
||||
IoMail,
|
||||
IoPencil,
|
||||
IoSearch
|
||||
} from 'react-icons/io5';
|
||||
import Link from 'next/link';
|
||||
import ThemeSwitcher from '@/components/ThemeSwitcher';
|
||||
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() {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
|
||||
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(
|
||||
[
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.4.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"framer-motion": "^11.3.24",
|
||||
"lxgw-wenkai-webfont": "^1.7.0",
|
||||
"motion": "^12.3.1",
|
||||
"next": "^15.0.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -20,6 +20,7 @@
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use": "^17.6.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.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)
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const SITE_CONFIG = {
|
||||
name: 'Atelier Lumiaethor',
|
||||
desc: ''
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
|
||||
Cargo.lock
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "constants"
|
||||
name = "gateway"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -1,24 +1,17 @@
|
||||
# server {
|
||||
# listen 80;
|
||||
# server_name lumither.com;
|
||||
|
||||
# return 301 https://$host$request_uri;
|
||||
# }
|
||||
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
# listen 443 ssl;
|
||||
# server_name lumither.com;
|
||||
listen 443 ssl;
|
||||
server_name lumither.com;
|
||||
|
||||
# ssl_certificate certs/cert.pem;
|
||||
# ssl_certificate_key certs/key.pem;
|
||||
|
||||
# ssl_session_cache shared:SSL:1m;
|
||||
# ssl_session_timeout 5m;
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location / {
|
||||
default_type text/html;
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf /opt/openresty/nginx/lua
|
||||
cp -r ./lua /opt/openresty/nginx
|
||||
@@ -1 +0,0 @@
|
||||
ngx.say("hello world")
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# refresh env
|
||||
/bin/bash ./flush_env.sh
|
||||
|
||||
# start frontend
|
||||
cd frontend || exit
|
||||
screen -S nocturne_frontend -d -m bun dev
|
||||
cd ..
|
||||
|
||||
# start backend
|
||||
cd backend || exit
|
||||
screen -S nocturne_backend -d -m cargo r
|
||||
cd ..
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/env bash
|
||||
|
||||
cp .env ./backend
|
||||
cp .env ./frontend
|
||||
cp .env.local ./backend
|
||||
cp .env.local ./frontend
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
|
||||
if [ -z "$BLOG_ENDPOINT" ]; then
|
||||
echo "BLOG_ENDPOINT is not set in the .env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$GIT_WORK_DIR" ]; then
|
||||
echo "GIT_WORK_DIR is not set in the .env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$BACKEND_PORT" ]; then
|
||||
echo "BACKEND_PORT is not set in the .env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_for_updates() {
|
||||
cd "$GIT_WORK_DIR" || exit
|
||||
res=$(git pull)
|
||||
if echo "$res" | grep -q "Already up to date"; then
|
||||
echo "Repo is up to date."
|
||||
else
|
||||
echo "Repo has updated; calling db update"
|
||||
curl --request POST -sL \
|
||||
--url "localhost:$BACKEND_PORT"
|
||||
#todo: to be continued
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -d "$GIT_WORK_DIR" ]; then
|
||||
check_for_updates
|
||||
else
|
||||
git clone "$BLOG_ENDPOINT" "$GIT_WORK_DIR"
|
||||
if [ $? -eq 0 ]; then
|
||||
check_for_updates
|
||||
else
|
||||
echo "Failed to clone the repo"
|
||||
fi
|
||||
fi
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
. "$DIR/.env"
|
||||
|
||||
sudo groupadd nocturne
|
||||
USERNAME="$(whoami)"
|
||||
echo "$USERNAME"
|
||||
sudo usermod -aG nocturne "$USERNAME"
|
||||
|
||||
sudo mkdir -p "$LOG_ROOT_DIR/backend" "$LOG_ROOT_DIR/frontend"
|
||||
sudo chown -R :nocturne "$LOG_ROOT_DIR"
|
||||
sudo chmod -R 775 "$LOG_ROOT_DIR"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# refresh env
|
||||
/bin/bash ./flush_env.sh
|
||||
|
||||
# start frontend
|
||||
(
|
||||
cd frontend || exit
|
||||
bun i
|
||||
bun next build
|
||||
screen -S nocturne_frontend -d -m bun start
|
||||
cd ..
|
||||
) &
|
||||
|
||||
# start backend
|
||||
(
|
||||
cd backend || exit
|
||||
screen -S nocturne_backend -d -m cargo r -r
|
||||
cd ..
|
||||
) & wait
|
||||
Reference in new issue
Block a user