18 Commits
Author SHA1 Message Date
lumither 4309f42bf7 feat(frontend) qwik init 2025-02-17 15:50:53 -08:00
lumither eef32ad695 feat(backend, lb) add missing components 2025-02-17 15:28:33 -08:00
lumither acd0525513 feat(backend) rebuild start 2025-02-17 15:27:41 -08:00
lumither 8938c96409 feat(frontend) add Activities.tsx 2025-02-05 13:57:20 -08:00
lumither 6da6231c92 feat(lab) add link to lab 2024-12-10 04:00:57 -08:00
lumither 19f84d2188 fix(blog::post::footer) remove update_date section if undefined 2024-12-10 04:00:10 -08:00
lumither 20c2964242 feat(container) production build 2024-12-08 22:25:47 -08:00
lumither eac84307bf Merge pull request #1 from Lumither/backend-refactor
Backend refactor
2024-12-01 02:39:00 -08:00
lumither a0dc40d2ca feat(blog::api) fix apis, frontend 2024-12-01 02:35:13 -08:00
lumither cbc831bb70 fix(cron::update) fix several errors 2024-11-27 04:00:38 -08:00
lumither bacaea80a3 feat(cron::update) auto update git repo and update db index 2024-11-27 00:18:08 -08:00
lumither 99a19d8d9e feat(init::schema) init database schema 2024-11-20 21:38:36 -08:00
lumither 7f400470ff feat(cron) call the module 2024-11-20 19:26:33 -08:00
lumither e26003f430 ref(global) move modules, extract constants 2024-11-20 00:57:54 -08:00
lumither c7e3b12856 feat(macros) add panic_with_log 2024-11-19 00:33:09 -08:00
lumither a123ff9f3f fix(scheduler) fn type 2024-11-17 21:06:36 -08:00
lumither d98aa3f745 fix(scheduler) remove unnecessary field 2024-10-30 23:07:22 -07:00
lumither 6f45a140ba ref(scheduler) move module 2024-10-30 22:54:39 -07:00
112 changed files with 8028 additions and 2723 deletions

No files matched your search

+9 -9
View File
@@ -1,19 +1,19 @@
# postgres settings
DB_HOST=localhost
DB_PORT=5432
POSTGRES_PASSWORD=
POSTGRES_USER=
#POSTGRES_PASSWORD=
POSTGRES_USER=nocturne
POSTGRES_DB=nocturne
POSTGRES_HOST=postgres
# backend settings
#GIT_WORK_DIR=../blog_posts_git
#LOG_ROOT_DIR=/var/log/nocturne
BLOG_ENDPOINT=https://github.com/Lumither/blog-posts.git
WORK_DIR=~/.nocturne
BACKEND_PORT=3001
#BLOG_ENDPOINT=https://github.com/Lumither/blog-posts.git
BLOG_GIT_URL=https://github.com/Lumither/blog-posts.git
WORK_DIR=/nocturne
#BACKEND_PORT=3001
# frontend settings
API_LOCAL_URL=http://localhost:3001
API_REMOTE_URL=https://lumither.com/api
API_LOCAL_URL=http://backend:3001
API_REMOTE_URL=https://lumither.com/api
+13 -3
View File
@@ -1,6 +1,16 @@
ddev:
ARCH := $(shell uname -m)
ifeq ($(ARCH), arm64)
ARCH := aarch64
endif
all: release
release:
docker compose -f prod.compose.yaml build --build-arg ARCH=$(ARCH)
dev:
docker-compose -f dev.compose.yaml up --build
dclean:
docker-compose -f dev.compose.yaml rm -fsv
clean:
docker-compose -f prod.compose.yaml -f dev.compose.yaml rm -fsv
+10 -30
View File
@@ -1,36 +1,16 @@
# `Nocturne: personal website & CMS`
# `Nocturne: Personal Website`
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
<p align="center">
<img src="https://avatars.githubusercontent.com/u/46409277?v=4" alt="GitHub Avatar"/>
</p>
> [!NOTE]
> This system is not production ready, and contains bugs, (security risk potentially).
> But have fun on it =)
*will add more here in the future
## 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...
+2
View File
@@ -1 +1,3 @@
/target
/Cargo.lock
+1 -2
View File
@@ -1,8 +1,7 @@
[workspace]
resolver = "2"
members = [
"api_server",
"constants",
"server",
"macros",
"markdown",
]
-22
View File
@@ -1,22 +0,0 @@
[package]
name = "api_server"
version = "0.1.0"
edition = "2021"
[dependencies]
constants = { path = "../constants" }
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 = "0.7.5"
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"] }
sha2 = "0.10.8"
cron = "0.12.1"
@@ -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)))
}
@@ -1,88 +0,0 @@
use constants::blog::PAGE_SIZE;
use axum::{extract::Query, extract::State, http::StatusCode, Json};
use chrono::{DateTime, Utc};
use futures::future::join_all;
use serde::Deserialize;
use serde_json::{json, Map, Value};
use sqlx::{postgres::PgRow, query, PgPool, Row};
use tracing::error;
use uuid::Uuid;
#[derive(Deserialize)]
pub struct Params {
page: Option<u32>,
}
pub async fn get_post_list(
State(db_connection): State<PgPool>,
Query(params): Query<Params>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
let search_param_page = params.page.unwrap_or(1);
if search_param_page < 1 {
return Err((
StatusCode::BAD_REQUEST,
Json::from(json!({"error": "`page` should be greater than 0"})),
));
}
let offset = PAGE_SIZE * (search_param_page - 1);
let post_list = sqlx::query("SELECT * FROM post ORDER BY first_update DESC LIMIT $1 OFFSET $2")
.bind(PAGE_SIZE as i32)
.bind(offset as i32)
.fetch_all(&db_connection)
.await;
let post_list = match post_list {
Ok(post_list) => post_list,
Err(e) => {
error!(
"Database query error on fetching post list: {}",
e.to_string()
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json::from(json!({"error": e.to_string()})),
));
}
};
let mut res = Map::new();
let posts: Vec<_> = post_list
.into_iter()
.map(|post| analyze_post(&db_connection, post))
.collect();
let posts = join_all(posts).await;
res.insert("posts".to_string(), Value::from(posts));
Ok(Json::from(Value::from(res)))
}
async fn analyze_post(db_connection: &PgPool, post: PgRow) -> Value {
let post_id: Uuid = post.get("post_id");
let tags = fetch_post_tags(db_connection, post_id).await;
json!({
"post_id": post_id.to_string(),
"title": post.get::<String, _>("title"),
"category": post.get::<String, _>("category"),
"header_img": post.get::<String, _>("header_img"),
"sub_title": post.get::<String, _>("sub_title"),
"summary": post.get::<String, _>("summary"),
"last_update": post.get::<DateTime<Utc>, _>("last_update"),
"first_update": post.get::<DateTime<Utc>, _>("first_update"),
"tags": tags,
})
}
async fn fetch_post_tags(db_connection: &PgPool, post_id: Uuid) -> Vec<String> {
match query("SELECT tag FROM Tag WHERE post_id = $1")
.bind(post_id)
.fetch_all(db_connection)
.await
{
Ok(res) => res.iter().map(|tag| tag.get::<String, _>("tag")).collect(),
Err(_) => vec![],
}
}
-4
View File
@@ -1,4 +0,0 @@
pub mod get_page_count;
pub mod get_post;
pub mod get_post_list;
pub mod refresh;
-190
View File
@@ -1,190 +0,0 @@
use std::path::Path;
use std::str::FromStr;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::Json;
use serde::Deserialize;
use serde_json::{Map, Value};
use sqlx::PgPool;
// use crate::api::post::errors::refresh::PostIdxError;
// use crate::api::post::utils::{build_post_idx, post};
// use crate::model::blog::{POST_BASE_MODEL, POST_HASH_MODEL, POST_META_MODEL, TAG_MODEL};
#[derive(Deserialize)]
pub struct Params {
/// refresh level
level: Option<String>,
}
pub async fn refresh(
State(db_connection): State<PgPool>,
Query(params): Query<Params>,
) -> Result<StatusCode, (StatusCode, Json<Value>)> {
// let is_force_refresh = params.level.unwrap_or("".to_string()) == "force";
//
// // search post
// let data_repo_dir = match env::var("GIT_WORK_DIR") {
// Ok(value) => value,
// Err(_) => {
// return Err((
// StatusCode::INTERNAL_SERVER_ERROR,
// Json::from(json!((
// "error",
// "error at parsing environment variable: GIT_WORK_DIR"
// ))),
// ));
// }
// };
// let post_dir = Path::new(&data_repo_dir).join("posts");
//
// if is_force_refresh {
// if let Err(e) = query("DROP TABLE IF EXISTS hash, meta, post, tag;")
// .execute(&db_connection)
// .await
// {
// error!(
// "DB query error when deleting table: `hash`, `meta`, `post`, `tag`: {}",
// e.to_string()
// );
// return Err((
// StatusCode::INTERNAL_SERVER_ERROR,
// Json::from(json!(("error", e.to_string()))),
// ));
// }
// }
//
// // generate post idx
// let post_path = search_md(&post_dir);
//
// let post_list_with_hash: Vec<(Map<_, _>, Vec<_>)> = post_path
// .into_iter()
// .filter_map(|post_path| match post::from_path(&post_path) {
// Ok(post) => match fs::read(&post_path) {
// Ok(bytes) => {
// let mut hasher = Sha256::new();
// hasher.update(bytes);
// let hash = hasher.finalize().to_vec();
// Some((post, hash))
// }
// Err(e) => {
// error!(
// "Failed to read file while hashing the post <{}>, skipping: {}",
// post_path,
// e.to_string()
// );
// None
// }
// },
// Err(e) => {
// error!(
// "Failed to parse post <{}>, skipping: {}",
// post_path,
// e.to_string()
// );
// None
// }
// })
// .collect();
//
// // db create
// 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();
//
// let res: Vec<_> = stream::iter(post_list_with_hash)
// .map(|(post, hash)| {
// let db_clone = db_connection.clone();
// async move { build_post_idx(&db_clone, post, hash).await }
// })
// .buffer_unordered(usize::MAX)
// .collect()
// .await;
//
// let errs: Vec<_> = res
// .into_iter()
// .filter_map(|res| res.err())
// .map(|e| e.to_string())
// .collect();
//
// if !errs.is_empty() {
// Err((
// StatusCode::INTERNAL_SERVER_ERROR,
// Json::from(json!({"error": errs})),
// ))
// } else {
// Ok(StatusCode::OK)
// }
todo!()
}
/// Search a dir, return a list of markdown files
///
/// # Arguments
/// * `entry`: dir path
///
/// # Returns
/// Vec<String, Global>
/// * `String`: path of a markdown file
///
fn search_md(entry: &Path) -> Vec<String> {
if entry.ends_with(".git") {
return vec![];
}
if entry.is_dir() {
if let Ok(files) = entry.read_dir() {
files
.flat_map(|file| {
let file_path = file.unwrap().path();
search_md(file_path.as_path())
})
.collect()
} else {
vec![]
}
} else if entry.extension().is_some_and(|ext| ext == "md") {
vec![entry.to_str().unwrap().to_string()]
} else {
vec![]
}
}
async fn build_post_idx(
db_connection: &PgPool,
post: Map<String, Value>,
hash: Vec<u8>,
// ) -> Result<(), PostIdxError> {
) -> Result<(), ()> {
todo!()
// let meta = &post["meta"];
//
// // extract post id
// let post_id = match Uuid::from_str(post["post_id"].as_str().unwrap()) {
// Ok(id) => id,
// Err(e) => {
// return Err(PostIdxError::InvalidUUID {
// id: post["post_id"].as_str().unwrap().to_string(),
// err_msg: e.to_string(),
// })
// }
// };
//
// if build_post_idx::hash(db_connection, post_id, hash).await? {
// build_post_idx::base(db_connection, &post, post_id, meta).await?;
// build_post_idx::tag(db_connection, post_id, meta).await?;
// build_post_idx::meta(db_connection, post_id, meta).await?;
// }
//
// Ok(())
}
-19
View File
@@ -1,19 +0,0 @@
mod api;
mod scheduler;
use crate::blog::api::{
get_page_count::get_page_count, get_post::get_post, get_post_list::get_post_list,
};
use crate::blog::api::refresh::refresh;
use axum::routing::{get, post};
use axum::Router;
use sqlx::{Pool, Postgres};
pub fn get_router() -> Router<Pool<Postgres>> {
Router::new()
.route("/refresh_posts", post(refresh))
.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,184 +0,0 @@
use crate::blog::{
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 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>>>>,
stopped: Arc<RwLock<Vec<Uuid>>>,
}
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>>>>,
stopped: Arc<RwLock<Vec<Uuid>>>,
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);
}
stopped.write()?.push(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 {
stopped.write()?.push(id);
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(), self.stopped.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 stopped_clone = self.stopped.clone();
let task_clone = task.clone();
thread::spawn(move || Self::run_task(running_clone, stopped_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)
}
}
#[cfg(test)]
mod tests {
use crate::blog::scheduler::tasks::basic::BasicTask;
use crate::blog::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.run_non_blocking(&id)?;
thread::sleep(Duration::from_secs(4));
scheduler.stop(&id)?;
// test termination
thread::sleep(Duration::from_secs(5));
Ok(())
}
}
@@ -1,44 +0,0 @@
use crate::blog::scheduler::scheduler_error::SchedulerError::MutexPoison;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::sync::PoisonError;
pub enum SchedulerError {
IdNotFound,
IdAlreadyRunning,
MutexPoison(String),
}
impl<T> From<PoisonError<T>> for SchedulerError {
fn from(value: PoisonError<T>) -> Self {
MutexPoison(value.to_string())
}
}
impl Debug for SchedulerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
impl Display for SchedulerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
SchedulerError::IdNotFound => {
"SchedulerError::IdNotFound".to_string()
}
SchedulerError::IdAlreadyRunning => {
"SchedulerError::IdAlreadyRunning".to_string()
}
MutexPoison(str) => {
format!("SchedulerError::MutexPoison: {}", str)
}
}
)
}
}
impl Error for SchedulerError {}
@@ -1,77 +0,0 @@
use chrono::{DateTime, Utc};
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use std::time::Duration;
#[derive(Clone, Debug, Default)]
pub struct SkippableSleeper {
pub duration: Duration,
state: Arc<(Mutex<bool>, Condvar)>,
}
impl SkippableSleeper {
pub fn new(duration: Duration) -> Self {
SkippableSleeper {
duration,
state: Arc::new((Mutex::new(false), Condvar::new())),
}
}
// Blocks until the duration has passed or skip() is called
pub fn start(&self) {
let (lock, cvar) = &*self.state;
let mut received = lock.lock().unwrap();
let state_clone = Arc::clone(&self.state);
let duration_clone = self.duration;
thread::spawn(move || {
thread::sleep(duration_clone);
let (lock, cvar) = &*state_clone;
let mut received = lock.lock().unwrap();
*received = true;
cvar.notify_one();
});
while !*received {
received = cvar.wait(received).unwrap();
}
}
// Skip the blocking delay
pub fn skip(&self) {
let (lock, cvar) = &*self.state;
let mut received = lock.lock().unwrap();
*received = true;
cvar.notify_one();
}
}
impl From<DateTime<Utc>> for SkippableSleeper {
fn from(value: DateTime<Utc>) -> Self {
let from_utc_timestamp = value.timestamp() as u64;
let curr_utc_timestamp = Utc::now().timestamp() as u64;
SkippableSleeper::new(Duration::from_secs(from_utc_timestamp - curr_utc_timestamp))
}
}
#[cfg(test)]
mod test {
use crate::blog::scheduler::sleeper::SkippableSleeper;
use std::thread;
use std::time::Duration;
#[test]
fn usage() {
let sleeper = SkippableSleeper::new(Duration::new(10, 0));
thread::spawn({
let sleeper = sleeper.clone();
move || {
println!("Sleeping...");
sleeper.start();
println!("Woke up!");
}
});
println!("Start Sleeping...");
thread::sleep(Duration::from_secs(4));
println!("Skipping sleep...");
sleeper.skip();
}
}
@@ -1,3 +0,0 @@
pub trait TaskFunc: Fn() + Send + Sync + 'static {}
impl<T> TaskFunc for T where T: Fn() + Send + Sync + Clone + 'static {}
@@ -1,30 +0,0 @@
use crate::blog::scheduler::task_func::TaskFunc;
use crate::blog::scheduler::tasks::Task;
use chrono::{DateTime, Utc};
use cron::Schedule;
use std::error::Error;
use std::str::FromStr;
pub struct BasicTask {
schedule: Schedule,
func: Box<dyn TaskFunc>,
}
impl BasicTask {
pub fn new(func: fn(), cron_expr: &str) -> Result<Self, Box<dyn Error>> {
Ok(BasicTask {
schedule: Schedule::from_str(cron_expr)?,
func: Box::new(func),
})
}
}
impl Task for BasicTask {
fn get_next_execution(&self) -> Option<DateTime<Utc>> {
self.schedule.upcoming(Utc).next()
}
fn call(&self) {
(self.func)()
}
}
@@ -1,9 +0,0 @@
use crate::blog::scheduler::task_func::TaskFunc;
use chrono::{DateTime, Utc};
pub mod basic;
pub trait Task: Send + Sync {
fn get_next_execution(&self) -> Option<DateTime<Utc>>;
fn call(&self);
}
-24
View File
@@ -1,24 +0,0 @@
use std::env;
use crate::logger;
use tracing_appender::non_blocking::WorkerGuard;
pub fn logger_init() -> (WorkerGuard, WorkerGuard) {
logger::init()
}
pub fn load_env() {
if let Some(env_file) = env::args().nth(1) {
match dotenv::from_filename(&env_file) {
Ok(_) => {
println!("[info] env loaded from {}, starting up", env_file);
}
Err(e) => {
panic!("[fatal] failed to read {}: {}", env_file, e);
}
}
} else {
println!("[info] no .env file referred, starting up")
}
}
-102
View File
@@ -1,102 +0,0 @@
use std::path::Path;
use std::{env, io};
use chrono::Local;
use dirs::home_dir;
use tracing::info;
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
use tracing_subscriber::{
filter::LevelFilter, fmt, fmt::format::Writer, fmt::time::FormatTime, fmt::MakeWriter,
layer::SubscriberExt, Layer, Registry,
};
struct LocalTimer;
impl FormatTime for LocalTimer {
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
write!(
w,
"{}",
Local::now().format(constants::time::DEFAULT_TIME_FORMAT)
)
}
}
type _LogWriter = dyn for<'writer> MakeWriter<'writer, Writer = dyn io::Write + 'static> + 'static;
pub fn init() -> (WorkerGuard, WorkerGuard) {
let log_path = &env::var("LOG_ROOT_DIR").unwrap_or_else(|e| {
if let Ok(default_log_path) = env::var("WORK_DIR") {
if default_log_path.starts_with('~') {
let home_path = home_dir()
.expect("[fatal] unable to locate home dir with parsing env `WORK_DIR`, try to use absolute path instead");
home_path
.join(default_log_path.strip_prefix("~/").unwrap())
.into_os_string()
.into_string()
.expect("[fatal] failed to parse env `WORK_DIR`, try to use absolute path instead")
} else {
default_log_path
}
} else {
panic!("[fatal] failed to parse env `LOG_ROOT_DIR`: {}", e);
}
});
let log_path = Path::new(log_path)
.join("log")
.join("backend")
.into_os_string()
.into_string()
.unwrap_or_else(|e| panic!("[fatal] failed to parse {:?}", e));
let (info_log_writer, info_guard) = non_blocking(rolling::never(&log_path, "info.log"));
let (err_log_writer, error_guard) = non_blocking(rolling::never(&log_path, "error.log"));
let info_format = tracing_subscriber::fmt::format()
.with_level(true)
.with_target(true)
.with_source_location(true)
.with_ansi(false);
let err_format = tracing_subscriber::fmt::format()
.with_level(true)
.with_target(true)
.with_source_location(true)
.with_ansi(false);
let stdout_format = tracing_subscriber::fmt::format()
.with_level(true)
.with_target(true)
.with_source_location(true)
.with_timer(LocalTimer);
// let default_filter = tracing_subscriber::filter::EnvFilter::try_from_default_env()
// .unwrap_or_else(|_| "tower_http=debug,axum::rejection=trace".into());
let subscribers = Registry::default()
.with(
fmt::Layer::default()
.with_writer(info_log_writer)
.event_format(info_format),
)
.with(
fmt::Layer::default()
.with_writer(err_log_writer)
.event_format(err_format)
.with_filter(LevelFilter::ERROR),
)
.with(
fmt::Layer::default()
.with_writer(io::stdout)
.with_ansi(true)
.event_format(stdout_format),
);
tracing::subscriber::set_global_default(subscribers)
.expect("[fatal] failed to setup logging system");
info!("module started: logger");
info!("logging to dir: {}", log_path);
(info_guard, error_guard)
}
-12
View File
@@ -1,12 +0,0 @@
mod blog;
mod init;
mod logger;
mod server;
#[tokio::main]
async fn main() {
init::load_env();
let _guards = init::logger_init();
server::start().await;
}
-65
View File
@@ -1,65 +0,0 @@
use std::env;
use std::num::ParseIntError;
use std::process::exit;
use std::str::FromStr;
use crate::blog;
use axum::Router;
use sqlx::{
postgres::{PgConnectOptions, PgPoolOptions},
ConnectOptions,
};
use tracing::{error, info, warn};
pub async fn start() {
let port: u32 = match env::var("BACKEND_PORT") {
Ok(value) => value.parse().unwrap_or_else(|e: ParseIntError| {
warn!("failed to parse BACKEND_PORT: {}", e.to_string());
3001
}),
Err(_) => 3001,
};
let db_connect_option = PgConnectOptions::from_str(&parse_db_uri())
.unwrap()
.disable_statement_logging();
let db_pool = match PgPoolOptions::new().connect_with(db_connect_option).await {
Ok(pool) => {
info!("database connected");
pool
}
Err(e) => {
error!("failed to load database: {}", e.to_string());
exit(1);
}
};
let app = Router::new()
.nest("blog", blog::get_router())
.with_state(db_pool);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
.await
.unwrap_or_else(|e| {
error!("failed to build TCP listener: {}", e.to_string());
exit(1);
});
axum::serve(listener, app).await.unwrap_or_else(|e| {
error!("failed to start axum: {}", e.to_string());
exit(1);
});
}
fn parse_db_uri() -> String {
let postgres_user = env::var("POSTGRES_USER").unwrap_or("".to_string());
let postgres_password = env::var("POSTGRES_PASSWORD").unwrap_or("".to_string());
let db_host = env::var("DB_HOST").unwrap_or("localhost".to_string());
let db_port = env::var("DB_PORT")
.unwrap_or("5432".to_string())
.to_string();
let postgres_db = env::var("POSTGRES_DB").unwrap_or("nocturne".to_string());
format!("postgresql://{postgres_user}:{postgres_password}@{db_host}:{db_port}/{postgres_db}")
}
+2
View File
@@ -4,3 +4,5 @@ version = "0.1.0"
edition = "2021"
[dependencies]
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
+1 -14
View File
@@ -1,14 +1 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
mod log;
+64
View File
@@ -0,0 +1,64 @@
/// log the message at specific log level and exit(1)
///
/// # Example
/// ```rust
///
/// use macros::panic_with_log;
/// use tracing::Level;
///
/// panic_with_log!(Level::ERROR, "test panicked: {}", 42);
/// ```
#[macro_export]
macro_rules! panic_with_log {
($level:expr, $msg:literal $(, $($arg:tt)*)?) => {{
use std::process;
use tracing::event;
event!($level, $msg $(, $($arg)*)?);
process::exit(1);
}};
}
/// 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]
fn test_panic_with_log() {
use std::io;
use tracing::Level;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, Registry};
let format = tracing_subscriber::fmt::format()
.with_level(true)
.with_target(true)
.with_source_location(true);
let subscriber = Registry::default().with(
fmt::Layer::default()
.with_writer(io::stderr)
.event_format(format),
);
tracing::subscriber::set_global_default(subscriber).unwrap();
panic_with_log!(Level::ERROR, "Test panicked: {}", 42);
}
}
+2 -2
View File
@@ -5,10 +5,10 @@ edition = "2021"
[dependencies]
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
chrono = { version = "0.4.38", features = ["serde", "alloc"] }
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.128"
regex = "1.10.4"
yaml-rust2 = "0.8.1"
yaml-rust2 = "0.9.0"
sha2 = "0.10.8"
base64 = "0.22.1"
thiserror = "2.0.3"
+16
View File
@@ -0,0 +1,16 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("missing front-matter field: id")]
MissingID,
#[error("invalid file id: {}", msg)]
InvalidID { msg: String },
#[error("invalid front-matter: {}", msg)]
InvalidFrontMatter { msg: String },
#[error("fs error: {}", msg)]
FSError { msg: String },
}
+26 -13
View File
@@ -1,35 +1,48 @@
mod error;
mod utils;
use std::error::Error;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use crate::error::Error;
use crate::utils::front_matter::{parse_front_matter, split_md_front_matter};
use base64::{engine::general_purpose, Engine};
use serde::Serialize;
use serde_json::Value;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::utils::front_matter::{parse_front_matter, split_md_front_matter};
#[derive(Default, Debug, Serialize)]
pub struct MdFile {
file_id: Uuid,
file_name: String,
meta: Value,
content: String,
hash: String,
pub file_id: Uuid,
pub filename: String,
pub meta: Value,
pub content: String,
pub hash: String,
}
impl MdFile {
pub async fn from_file(file_path: &str) -> Result<MdFile, Box<dyn Error>> {
let raw_file = fs::read_to_string(file_path)?;
pub fn from_file(file_path: &Path) -> Result<MdFile, Error> {
let raw_file = match fs::read_to_string(file_path) {
Ok(file) => file,
Err(e) => return Err(Error::FSError { msg: e.to_string() }),
};
let raw_file_bytes = raw_file.as_bytes().to_vec();
let (front_matter_str, content) = split_md_front_matter(raw_file);
let front_matter = parse_front_matter(&front_matter_str)?;
let front_matter = match parse_front_matter(&front_matter_str) {
Ok(front_matter) => front_matter,
Err(e) => return Err(Error::InvalidFrontMatter { msg: e.to_string() }),
};
let hash = calculate_hash_base64(&raw_file_bytes);
let file_id = Uuid::from_str(front_matter["id"].to_string().as_str())?;
let file_id = match front_matter["id"].as_str() {
Some(id) => match Uuid::from_str(id) {
Ok(id) => id,
Err(e) => return Err(Error::InvalidID { msg: e.to_string() }),
},
None => return Err(Error::MissingID),
};
let file_name = Path::new(file_path)
.file_name()
.unwrap()
@@ -39,7 +52,7 @@ impl MdFile {
Ok(MdFile {
file_id,
file_name,
filename: file_name,
meta: front_matter,
content,
hash,
+8 -4
View File
@@ -16,8 +16,12 @@ pub fn split_md_front_matter(raw_file: String) -> (String, String) {
}
pub fn parse_front_matter(front_matter: &str) -> Result<Value, Box<dyn Error>> {
let parsed_f_matter = YamlLoader::load_from_str(front_matter)?[0].clone();
Ok(Value::from(yaml_to_serde(&parsed_f_matter)))
let parsed_f_matter = YamlLoader::load_from_str(front_matter)?;
if parsed_f_matter.is_empty() {
Ok(Value::String(String::new()))
} else {
Ok(yaml_to_serde(&parsed_f_matter[0]))
}
}
#[cfg(test)]
@@ -60,7 +64,7 @@ tags: [ "test1", "test2" ]
#[test]
fn test_front_matter_parser() {
parse_front_matter(
dbg!(parse_front_matter(
r##"---
title: test_title
date: 0000-00-00+0000
@@ -71,7 +75,7 @@ summary: summary
tags: [ "test1", "test2" ]
---
"##,
)
))
.expect("Failed to parse front matter");
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
macros = { path = "../macros" }
tracing = "0.1.41"
git2 = "0.20.0"
[build-dependencies]
git2 = "0.20.0"
chrono = "0.4.39"
+15
View File
@@ -0,0 +1,15 @@
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_date = Utc::now().date_naive().format("%Y%m%d").to_string();
let build_id = format!("{}@{}", utc_date, &hash[..8]);
println!("cargo:rustc-env=BUILD_ID={}", build_id);
}
+32
View File
@@ -0,0 +1,32 @@
CREATE TABLE tag
(
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE category
(
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE post
(
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 category (id) NOT NULL
);
CREATE TABLE post_tag
(
post SERIAL REFERENCES post (id),
tag SERIAL REFERENCES tag (id)
)
File renamed without changes.
+48
View File
@@ -0,0 +1,48 @@
// Database related
pub mod database {
pub mod var_name {
pub const URI: &str = "DATABASE_URL";
pub const USERNAME: &str = "POSTGRES_USER";
pub const PASSWORD: &str = "POSTGRES_PASSWORD";
pub const HOST: &str = "POSTGRES_HOST";
pub const PORT: &str = "POSTGRES_PORT";
pub const DBNAME: &str = "POSTGRES_DB";
}
pub mod default_value {
pub const USERNAME: &str = "";
pub const PASSWORD: &str = "";
pub const HOST: &str = "localhost";
pub const PORT: &str = "5432";
pub const DBNAME: &str = "nocturne";
pub const DATABASE_TYPE: &str = "postgresql";
}
}
pub mod server {
pub mod var_name {
pub const PORT: &str = "BACKEND_PORT";
pub const HOST: &str = "BACKEND_HOST";
}
pub mod default_value {
pub const PORT: u32 = 3001;
pub const HOST: &str = "0.0.0.0";
}
}
pub mod general {
pub mod var_name {
pub const LOG_DIR: &str = "LOG_DIR";
pub const WORK_DIR: &str = "WORK_DIR";
pub const BLOG_GIT_URL: &str = "BLOG_GIT_URL";
pub const BLOG_GIT_REMOTE_BRANCH: &str = "BLOG_GIT_REMOTE_BRANCH";
pub const BLOG_GIT_REMOTE_NAME: &str = "BLOG_GIT_REMOTE_NAME";
}
pub mod default_value {
pub const WORK_DIR: &str = "~/.nocturne";
pub const BLOG_GIT_REMOTE_BRANCH: &str = "master";
pub const BLOG_GIT_REMOTE_NAME: &str = "origin";
}
}
@@ -1,2 +1,3 @@
pub mod blog;
pub mod config;
pub mod time;
@@ -1 +1,2 @@
pub const DEFAULT_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z";
pub const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d%zT%H%M%S";
+7
View File
@@ -0,0 +1,7 @@
mod constants;
const BUILD_ID: &str = env!("BUILD_ID");
fn main() {
println!("{}", BUILD_ID);
}
+10
View File
@@ -0,0 +1,10 @@
FROM rust:latest AS builder
WORKDIR /app
COPY . .
RUN ["cargo", "build", "-r"]
FROM gcr.io/distroless/cc AS production
ARG ARCH
COPY --from=builder /app/target/release/api_server ./api_server
COPY --from=builder /usr/lib/${ARCH}-linux-gnu/libz.so.1 /usr/lib/${ARCH}-linux-gnu/
ENTRYPOINT ["./api_server"]
+15
View File
@@ -0,0 +1,15 @@
FROM node:22 AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs22 AS production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
ENV HOSTNAME="0.0.0.0"
CMD ["./server.js"]
+7 -7
View File
@@ -1,11 +1,11 @@
services:
frontend:
build:
context: ./frontend
context: ./frontend
dockerfile: ../container/dev/frontend.containerfile
restart: always
ports:
- 3000:3000
- "3000:3000"
volumes:
- ./frontend:/app
- /app/node_modules
@@ -14,15 +14,15 @@ services:
- ./container/dev/.env
dns:
- 1.1.1.1
backend:
build:
context: ./backend
build:
context: ./backend
dockerfile: ../container/dev/backend.containerfile
restart: always
ports:
- 3001:3001
- "3001:3001"
volumes:
- ./blog_posts_git:/app/blog_posts_git:ro
env_file:
-10
View File
@@ -1,10 +0,0 @@
/node_modules
/.next/
/out/
/build
.DS_Store
*.pem
.env*.local
+38
View File
@@ -0,0 +1,38 @@
**/*.log
**/.DS_Store
*.
.vscode/settings.json
.history
.yarn
bazel-*
bazel-bin
bazel-out
bazel-qwik
bazel-testlogs
dist
dist-dev
lib
lib-types
etc
external
node_modules
temp
tsc-out
tsdoc-metadata.json
target
output
rollup.config.js
build
.cache
.vscode
.rollup.cache
dist
tsconfig.tsbuildinfo
vite.config.ts
*.spec.tsx
*.spec.ts
.netlify
pnpm-lock.yaml
package-lock.json
yarn.lock
server
+42
View File
@@ -0,0 +1,42 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:qwik/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
ecmaVersion: 2021,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"prefer-spread": "off",
"no-case-declarations": "off",
"no-console": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"@typescript-eslint/consistent-type-imports": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn",
},
};
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}
+33 -34
View File
@@ -1,43 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Build
/dist
/lib
/lib-types
/server
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# Development
node_modules
.env
*.local
# testing
/coverage
# Cache
.cache
.mf
.rollup.cache
tsconfig.tsbuildinfo
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# local env files
.env*.local
# Editor
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# bun
bun.lockb
# ide related
.idea
/package-lock.json
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Yarn
.yarn/*
!.yarn/releases
+37
View File
@@ -0,0 +1,37 @@
**/*.log
**/.DS_Store
*.
.vscode/settings.json
.history
.yarn
bazel-*
bazel-bin
bazel-out
bazel-qwik
bazel-testlogs
dist
dist-dev
lib
lib-types
etc
external
node_modules
temp
tsc-out
tsdoc-metadata.json
target
output
rollup.config.js
build
.cache
.vscode
.rollup.cache
tsconfig.tsbuildinfo
vite.config.ts
*.spec.tsx
*.spec.ts
.netlify
pnpm-lock.yaml
package-lock.json
yarn.lock
server
+65
View File
@@ -0,0 +1,65 @@
# Qwik City App ⚡️
- [Qwik Docs](https://qwik.dev/)
- [Discord](https://qwik.dev/chat)
- [Qwik GitHub](https://github.com/QwikDev/qwik)
- [@QwikDev](https://twitter.com/QwikDev)
- [Vite](https://vitejs.dev/)
---
## Project Structure
This project is using Qwik with [QwikCity](https://qwik.dev/qwikcity/overview/). QwikCity is just an extra set of tools on top of Qwik to make it easier to build a full site, including directory-based routing, layouts, and more.
Inside your project, you'll see the following directory structure:
```
├── public/
│ └── ...
└── src/
├── components/
│ └── ...
└── routes/
└── ...
```
- `src/routes`: Provides the directory-based routing, which can include a hierarchy of `layout.tsx` layout files, and an `index.tsx` file as the page. Additionally, `index.ts` files are endpoints. Please see the [routing docs](https://qwik.dev/qwikcity/routing/overview/) for more info.
- `src/components`: Recommended directory for components.
- `public`: Any static assets, like images, can be placed in the public directory. Please see the [Vite public directory](https://vitejs.dev/guide/assets.html#the-public-directory) for more info.
## Add Integrations and deployment
Use the `npm run qwik add` command to add additional integrations. Some examples of integrations includes: Cloudflare, Netlify or Express Server, and the [Static Site Generator (SSG)](https://qwik.dev/qwikcity/guides/static-site-generation/).
```shell
npm run qwik add # or `yarn qwik add`
```
## Development
Development mode uses [Vite's development server](https://vitejs.dev/). The `dev` command will server-side render (SSR) the output during development.
```shell
npm start # or `yarn start`
```
> Note: during dev mode, Vite may request a significant number of `.js` files. This does not represent a Qwik production build.
## Preview
The preview command will create a production build of the client modules, a production build of `src/entry.preview.tsx`, and run a local server. The preview server is only for convenience to preview a production build locally and should not be used as a production server.
```shell
npm run preview # or `yarn preview`
```
## Production
The production build will generate client and server modules by running both client and server build commands. The build command will use Typescript to run a type check on the source code.
```shell
npm run build # or `yarn build`
```
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
function Layout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'flex p-7' }>
{ children }
</div>
);
}
export default Layout;
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch';
const Me = async () => {
return (
<div className={ 'p-5' }>
<MarkdownRenderer>
{ await fetchText('https://raw.githubusercontent.com/lumither/blog-posts/master/about/me.md') }
</MarkdownRenderer>
</div>
);
};
export default Me;
-58
View File
@@ -1,58 +0,0 @@
import { Card } from '@nextui-org/react';
import React from 'react';
import { CardBody } from '@nextui-org/card';
import Link from 'next/link';
const About = () => {
return (
<div className={ `flex flex-col w-full justify-center align-middle h-dvh` }>
<div className={ 'md:relative md:h-[130px] mb-4 md:mb-0' }>
<div className={ 'md:relative md:left-[20%] flex flex-col w-fit' }>
<p className={ 'text-pretty text-3xl' }>{ '讷,远道而来的' }<span
className={ 'break-keep underline decoration-sky-400 dark:decoration-sky-700 decoration-4 underline-offset-0' }>旅人</span>
</p>
<p className={ 'text-pretty md:text-2xl mt-1 md:ml-4' }>{ '愿意坐下来听个故事吗...' }</p>
<p className={ 'hidden md:block text-lg md:ml-8' }>{ '一个关于一位' }<span
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ '追寻' }<span
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ ',探索' }<span
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ '的故事' }
</p>
</div>
</div>
<div className={ `flex justify-center w-full h-[60%] md:h-[40%]` }>
<div
className={ `flex flex-col md:flex-row w-full md:w-[85%] lg:w-[90%] xl:w-[85%] p-4 gap-x-7 gap-y-5` }>
<Card
isHoverable
className={ 'w-full h-[45%] md:h-full' }
as={ Link }
href={ '/about/me' }
isPressable
>
<CardBody>
<div className={ 'h-full flex flex-col justify-center' }>
<p className={ 'text-center text-3xl h-fit' }>关于我</p>
</div>
</CardBody>
</Card>
<Card
isHoverable
className={ 'w-full h-[45%] md:h-full' }
as={ Link }
href={ '/about/website' }
isPressable
>
<CardBody>
<div className={ 'h-full flex flex-col justify-center' }>
<p className={ 'text-center text-3xl h-fit' }>关于这个网站</p>
</div>
</CardBody>
</Card>
</div>
</div>
<p className={ 'text-center' }>*目前还没想到如何设计这一页,先摸鱼会(笑)</p>
</div>
);
};
export default About;
-14
View File
@@ -1,14 +0,0 @@
import React from 'react';
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch';
const Website = async () => {
return (
<div className={ 'p-5' }>
<MarkdownRenderer>
{ await fetchText('https://raw.githubusercontent.com/lumither/blog-posts/master/about/website.md') }
</MarkdownRenderer>
</div>
);
};
export default Website;
-92
View File
@@ -1,92 +0,0 @@
import React from 'react';
import { Card, CardBody, CardFooter } from '@nextui-org/card';
import { Chip } from '@nextui-org/react';
import Image from 'next/image';
import Link from 'next/link';
import { MdCalendarMonth, MdNorthEast } from 'react-icons/md';
type Props = {
post: JSON
}
function BlogPostCard({ post }: Props) {
let title = (post as any)['title'];
let sub_title = (post as any)['sub_title'];
// let summary = (post as any)['summary'];
let category = (post as any)['category'];
let tags = (post as any)['tags'];
// let last_update = new Date((post as any)['last_update']);
let first_update = new Date((post as any)['first_update']);
let post_id = (post as any)['post_id'];
let header_img = (post as any)['header_img'];
return (
<div className={ `w-full` }>
<Card as={ Link } href={ `/blog/post/${ post_id }` }
className={ `group` }
isPressable
isHoverable
>
{ header_img !== '' && <div className={ `w-full overflow h-80 relative` }>
<Image
alt={ 'cover picture' }
src={ header_img }
className={ `w-full object-cover rounded-b-none z-10` }
loading={ 'eager' }
fill
></Image>
<Image
alt={ 'cover picture shadow' }
src={ header_img }
loading={ 'eager' }
className={ `w-full object-cover rounded-b-none scale-y-110 blur-2xl` }
fill
></Image>
</div> }
<CardBody className={ `overflow-visible` }>
<div className={ `flex flex-row justify-between` }>
<div className={ `mx-2 ${ header_img !== '' ? '-mt-6' : 'mt-2' }` }>
<Chip className={ `mb-2 z-30 text-lg` } radius={ `sm` } size={ 'lg' } color={ `secondary` }
variant={ `solid` }>{ category }</Chip>
<p className={ `text-4xl font-bold` }> { title }</p>
<p className={ `text-xl` }> { sub_title }</p>
</div>
<MdNorthEast className={ `mt-1 mr-2` } size={ `40px` } />
</div>
</CardBody>
<CardFooter className={ 'pt-0' }>
<div className={ `flex flex-row flex-wrap justify-between w-full` }>
<div>
<ul className={ `flex flex-row flex-wrap` }>
{
tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
</li>
))
}
</ul>
</div>
<div className={ `flex flex-row place-content-end mb-1 items-center mx-1` }>
<MdCalendarMonth />
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
{ first_update.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }
</p>
</div>
</div>
</CardFooter>
</Card>
</div>
);
}
export default BlogPostCard;
-1
View File
@@ -1 +0,0 @@
// todo: create component
-37
View File
@@ -1,37 +0,0 @@
'use client';
import React from 'react';
import { Pagination as NextUIPagination } from '@nextui-org/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
const Pagination = ({ total }: {
total: number,
}) => {
const parsePage = Number(useSearchParams().get('page'));
const currPage = parsePage > 0 ? parsePage : 1;
const router = useRouter();
const isMobile = useScreenSizeTrigger('md');
return (
<div className={ 'w-full p-4 pt-6 flex justify-center' }>
<NextUIPagination
initialPage={ currPage }
total={ total }
siblings={ isMobile ? 0 : 1 }
size={ isMobile ? 'sm' : 'md' }
showControls
showShadow
color={ 'default' }
variant={ 'light' }
onChange={ (page) => {
router.push(`?page=${ page }`, { scroll: false });
} }
/>
</div>
);
};
export default Pagination;
-42
View File
@@ -1,42 +0,0 @@
'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 { Card, CardBody } from '@nextui-org/card';
const PostList = async ({ page }: { page: number }) => {
try {
const res = await fetchNocturneApi(`/get_post_list?page=${ page }`);
let posts = (await res.json() as any)['posts'];
return (
// blog list
<div className={ 'w-full' }>
<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
initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
>
<BlogPostCard post={ post } />
</MotionDiv>
</li>)
) }
</ul>
</div>
);
} catch (e: any) {
return (
// todo: use Error.tsx
<Card className={ `w-full` }>
<CardBody>
Fatal: { e.toString() }
</CardBody>
</Card>);
}
};
export default PostList;
-21
View File
@@ -1,21 +0,0 @@
const LOCAL_ENDPOINT = process.env.API_LOCAL_URL;
const REMOTE_ENDPOINT = process.env.API_REMOTE_URL;
const NOCTURNE_ENDPOINT = ((isServerSide = true) => {
if (process.env.NODE_ENV === 'production') {
if (isServerSide) {
return LOCAL_ENDPOINT;
} else {
return REMOTE_ENDPOINT;
}
} else {
return LOCAL_ENDPOINT;
}
});
export async function fetchNocturneApi(path: string, isServerSideRequest = true) {
return await fetch(`${ NOCTURNE_ENDPOINT(isServerSideRequest) }${ path }`, {
cache: 'no-cache'
});
}
-17
View File
@@ -1,17 +0,0 @@
import React from 'react';
function Layout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'flex flex-auto p-7 pl-2 lg:pl-7 w-full justify-center' }>
<main className={ 'w-full' }>
{ children }
</main>
</div>
);
}
export default Layout;
-24
View File
@@ -1,24 +0,0 @@
import PostList from '@/app/blog/PostList';
import Pagination from '@/app/blog/Pagination';
import { fetchNocturneApi } from '@/app/blog/api';
async function Blog({ searchParams }: {
searchParams?: {
page?: number
};
}) {
const parsePage = Number(searchParams?.page);
const currPage = parsePage > 0 ? parsePage : 1;
const ret = await fetchNocturneApi(`/get_page_count`);
const pageCount = (await ret.json() as any)['res'] as number;
return (
<div className={ 'w-full min-w-full space-y-4' }>
<PostList page={ currPage } />
<Pagination total={ pageCount } />
</div>
);
}
export default Blog;
@@ -1,21 +0,0 @@
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@nextui-org/react';
import { IoArrowBackOutline } from 'react-icons/io5';
const ButtonGoBack = () => {
const router = useRouter();
return (
<Button
startContent={ <IoArrowBackOutline /> }
variant={ 'light' }
onClick={ () => router.back() }
>
{ 'Back' }
</Button>
);
};
export default ButtonGoBack;
-94
View File
@@ -1,94 +0,0 @@
import React from 'react';
import MinifyPostCard from '@/app/blog/post/[id]/MinifyPostCard';
import Link from 'next/link';
import { FaCreativeCommons } from 'react-icons/fa6';
type Props = {
post: any
}
const Footer = (props: Props) => {
const { post } = 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 = new Date(post['last_update']);
const cc = post['meta']['cc'] ?? 'by-nc-sa';
return (
<div className={ `w-full` }>
<div className={ 'flex transition bg-zinc-300/80 dark:bg-gray-600/25' }>
<div className={ `p-8 w-full overflow-hidden` }>
<div className={ 'relative' }>
<FaCreativeCommons
size={ '160px' }
className={ 'absolute right-0 top-0 -mt-[4.8rem] -mr-16 opacity-30 brightness-50 ' }
/>
</div>
<div className={ 'flex flex-col space-y-4 relative' }>
<div className={ 'z-[2]' }>
<p className={ 'text-xl font-bold' }>{ post['meta']['title'] }</p>
<Link href={ link }>
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
</Link>
</div>
<div className={ 'z-[2] flex flex-row flex-wrap gap-x-7 gap-y-2' }>
<div>
<p className={ 'text-lg font-bold' }>Author</p>
<Link href={ author_link }>
<p className={ 'underline decoration-indigo-500 dark:decoration-sky-500' }>{ author }</p>
</Link>
</div>
<div>
<p className={ 'text-lg font-bold' }>Posted on</p>
<p>{ post_date.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }</p>
</div>
<div>
<p className={ 'text-lg font-bold' }>Updated on</p>
<p>{ update_date.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }</p>
</div>
<div>
<p className={ 'text-lg font-bold' }>Published under</p>
<Link href={ `https://creativecommons.org/licenses/${ cc }/4.0/` }>
<p className={ 'underline decoration-indigo-500 dark:decoration-sky-500' }>{ `CC ${ cc.toUpperCase() } 4.0` }</p>
</Link>
</div>
</div>
</div>
</div>
</div>
<div className={ `flex justify-center` }>
<div className={ `w-full shadow-2xl ` }>
<div className={ `flex flex-col md:flex-row w-full` }>
{
next_post &&
<MinifyPostCard post={ next_post } desc={ 'prev' }></MinifyPostCard>
}
{
prev_post &&
<MinifyPostCard post={ prev_post } desc={ 'next' }></MinifyPostCard>
}
</div>
</div>
</div>
</div>
);
};
export default Footer;
-80
View File
@@ -1,80 +0,0 @@
import React from 'react';
import Image from 'next/image';
import { Chip } from '@nextui-org/react';
import { MdCalendarMonth } from 'react-icons/md';
type Props = {
post: any
};
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['header_img'];
return (
<>
{ header_img !== '' && <div className={ `w-full overflow h-96 relative` }>
<Image
alt={ 'cover picture' }
src={ header_img }
className={ `w-full object-cover rounded-b-none z-[5]` }
fill
></Image>
<Image
alt={ 'cover picture' }
src={ header_img }
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
fill
></Image>
</div> }
<div
className={ 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' }>
<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>
</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) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
</li>
))
}
</ul>
</div>
<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', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }
</p>
</div>
</div>
</div>
</>
);
};
export default Headers;
@@ -1,58 +0,0 @@
import React from 'react';
import { Card, CardFooter, CardHeader } from '@nextui-org/card';
import Image from 'next/image';
import Link from 'next/link';
type Props = {
post: any
desc: string
}
const MinifyPostCard = (props: Props) => {
const { post, desc } = props;
const img_url = post['header_img'] as string;
return (
<div className={ 'w-full transition hover:brightness-75' }>
<Card isFooterBlurred
className="w-full h-[300px] shadow-none border-0 rounded-none overflow-hidden"
as={ Link }
isPressable
href={ `/blog/post/${ post['id'] }` }
>
<CardHeader className="absolute z-10 top-1 flex-col items-start">
<p className={ `text-tiny uppercase font-bold ${ img_url ? 'text-white/60' : 'text-black/60 dark:text-white/60' }` }>{ desc }</p>
</CardHeader>
{ img_url &&
<div className={ 'h-full' }>
<div className={ 'relative h-[300px] overflow-hidden' }>
<Image
className={ 'object-cover object-center ' }
src={ img_url }
alt={ post.title }
fill
/>
</div>
<CardFooter
className="absolute transition-colors bg-zinc-300/80 dark:bg-gray-600/25 dark:backdrop-brightness-75 bottom-0 justify-between rounded-none">
<div>
<p className="text-2xl font-bold">{ post['title'] }</p>
<p className="">{ post['sub_title'] }</p>
</div>
</CardFooter>
</div>
}
{ !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>
</div>
}
</Card>
</div>
);
};
export default MinifyPostCard;
-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(`/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;
-25
View File
@@ -1,25 +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';
function BlogReader({ params }: { params: { 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={ params.id } />
</div>
<div className={ 'mt-3' }>
<ButtonGoBack />
</div>
</MotionDiv>
);
}
export default BlogReader;
-17
View File
@@ -1,17 +0,0 @@
export async function fetchText(url: string, ttl: number = 600) {
const res = await fetch(url, {
next: {
revalidate: ttl
}
});
return await res.text();
}
export async function fetchJson(url: string, ttl: number = 600) {
const res = await fetch(url, {
next: {
revalidate: ttl
}
});
return await res.json();
}
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
function Layout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'p-7' }>
{ children }
</div>
);
}
export default Layout;
-93
View File
@@ -1,93 +0,0 @@
import React from 'react';
import { fetchJson } from '@/app/fetch';
import { Card, CardBody } from '@nextui-org/react';
import Image from 'next/image';
import Link from 'next/link';
export default async function Friends() {
const list = await fetchJson('https://raw.githubusercontent.com/Lumither/friends/master/friends.json');
const listLength = list.length;
const listMaxIdx = listLength - 1;
const listIsOdd = listLength % 2 !== 0;
return (
<div>
<div className={ `min-h-dvh pb-10` }>
<div className={ 'p-7 pt-[10%] md:pt-[20%]' }>
<div className={ 'flex flex-col md:flex-row gap-x-3 justify-center' }>
<p className={ 'text-3xl text-center text-balance' }>海内存知己</p>
<p className={ 'text-3xl text-center text-balance' }>天涯若比邻</p>
</div>
<p className={ 'text-center text-balance text-zinc-500 dark:text-zinc-400' }>{ 'Though far apart, kin at heart' }</p>
</div>
<div className={ 'flex w-full h-full justify-center pt-3' }>
<ul className={ 'grid grid-cols-2 gap-x-6 gap-y-3 md:max-w-[90%] xl:max-w-[85%] w-full' }>
{
list.map((friend: any, key: number) => (
<li
key={ key }
className={ (key === listMaxIdx && listIsOdd) ? 'col-span-2 flex justify-center' : 'col-span-2 md:col-span-1' }
>
<Card
isHoverable
isPressable
shadow={ 'none' }
as={ Link }
href={ friend['url'] }
className={ 'dark:bg-[#282830] w-full md:w-fit max-w-full shadow' }
>
<CardBody className={ 'overflow-visible pb-0.5' }>
<div className={ 'flex flex-row' }>
<div className={ 'flex p-2' }>
<div
className={ 'h-[80px] w-[80px] relative rounded-2xl' }>
<Image
alt="avatar"
className="object-cover rounded-2xl z-10"
fill
unoptimized
src={ friend['avatar'] }
/>
<Image
alt="avatar"
className="relative object-cover rounded-2xl scale-110 blur-3xl"
fill
unoptimized
src={ friend['avatar'] }
/>
</div>
</div>
<div className={ 'p-2 flex-col max-w-full' }>
<p className={ 'text-xl' }>{ friend['title'] }</p>
{ (friend['name'] === undefined || friend['name'].isNull) ??
<p className={ 'text-zinc-600 dark:text-zinc-400' }>{ `@ ${ friend['name'] }` }</p> }
{ (friend['desc'] === undefined || friend['desc'].isNull) ??
<p className={ 'text-sm text-pretty text-zinc-600 dark:text-zinc-300 mt-1' }>{ friend['desc'] }</p> }
</div>
</div>
</CardBody>
<div className={ 'h-2 relative w-full' }>
<Image
alt="theme bar"
draggable={ false }
className="object-cover object-center scale-110 blur-lg"
fill
unoptimized
src={ friend['avatar'] }
/>
</div>
</Card>
</li>
))
}
</ul>
</div>
</div>
<p className={ 'text-center' }>想要成为好伙伴嘛,向<span
className={ 'text-blue-800 dark:text-blue-500' }><Link
href={ 'https://github.com/Lumither/friends/' }>这里</Link></span>发起PR吧
</p>
</div>
);
}
-13
View File
@@ -1,13 +0,0 @@
@import "lxgw-wenkai-webfont/style.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: "LXGW WenKai", sans-serif;
}
pre, code {
font-family: "LXGW WenKai Mono", sans-serif;
}
-34
View File
@@ -1,34 +0,0 @@
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';
export const metadata: Metadata = {
title: 'Lumitherの酒馆',
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...'
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<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>
);
}
-10
View File
@@ -1,10 +0,0 @@
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
import React from 'react';
export default function Home() {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
}
-12
View File
@@ -1,12 +0,0 @@
'use client';
import { NextUIProvider } from '@nextui-org/react';
import React from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NextUIProvider>
{ children }
</NextUIProvider>
);
}
-64
View File
@@ -1,64 +0,0 @@
import React from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkToc from 'remark-toc';
import remarkFrontmatter from 'remark-frontmatter';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
const MarkdownRenderer = ({ children }: { children: React.ReactNode }) => {
return (
<div className={ 'w-full' }>
<Markdown
className={ `max-w-full text-justify text-pretty prose prose-sm prose-neutral dark:prose-invert
md:prose-md lg:prose-lg prose-code:overflow-x-scroll`
}
remarkPlugins={ [ remarkGfm, remarkToc, remarkFrontmatter ] }
rehypePlugins={ [ rehypeRaw, rehypeSanitize ] }
components={ {
pre(props) {
const { node, className, children, ...rest } = props;
if ((children as any)['type'] === 'code') {
const match = /language-(\w+)/.exec((children as any)['props']['className'] || '');
let lang = match ? match[1] : 'text';
return (
<pre className={ `not-prose` }>
<SyntaxHighlighter
// @ts-ignore
style={ oneDark }
language={ lang }
showLineNumbers
// wrapLongLines
// wrapLines
classNames={ className }
PreTag="div"
{ ...rest }
>
{
String((children as any)['props']['children']).replace(/\n$/, '')
}
</SyntaxHighlighter>
</pre>
);
} else {
return (
<pre className={ `not-prose` }>
<code className={ className } { ...props }>
{ children }
</code>
</pre>
);
}
}
} }
>
{
children as any
}
</Markdown>
</div>
);
};
export default MarkdownRenderer;
-5
View File
@@ -1,5 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export const MotionDiv = motion.div;
-35
View File
@@ -1,35 +0,0 @@
import { useEffect, useState } from 'react';
const screenSize = new Map<string, number>(
[
[ 'sm', 640 ],
[ 'md', 768 ],
[ 'lg', 1024 ],
[ 'xl', 1280 ],
[ '2xl', 2536 ]
]
);
export const useScreenSizeTrigger = (size: number | string) => {
const criticalSize = (typeof size === 'string') ? (screenSize.get(size) ?? 768) : size;
const [ isCritical, setIsCritical ] = useState(false);
useEffect(() => {
const updateMobileWidth = () => {
if (window.innerWidth < criticalSize) {
setIsCritical(true);
} else {
setIsCritical(false);
}
};
updateMobileWidth();
window.addEventListener('resize', updateMobileWidth);
return () => {
window.removeEventListener('resize', updateMobileWidth);
};
});
return isCritical;
};
-7
View File
@@ -1,7 +0,0 @@
export type Result<T, E> = {
ok: true;
value: T;
} | {
ok: false;
error: E;
};
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
const Search = () => {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
};
export default Search;
@@ -1,13 +0,0 @@
import React from 'react';
const ConstructionPlaceHolder = () => {
return (
<div className={ `flex h-full w-full justify-center items-center` }>
<p className={ `text-2xl font-bold` }>
正在施工中...
</p>
</div>
);
};
export default ConstructionPlaceHolder;
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import Link from 'next/link';
import { IoLogoGithub } from 'react-icons/io5';
const Footer = () => {
return (
<div className={ `p-5 mb-4` }>
<div className={ `h-1 w-20 bg-zinc-500 dark:bg-zinc-400 mb-3` }></div>
<p>
© 2024-{ new Date().getFullYear() } <Link href={ `/about` }>Lumither Tao</Link>
</p>
<p>
Powered by Next.js and Rust, built with passion and love
</p>
<Link
href={ `https://github.com/Lumither/blog` }
aria-label={ `GitHub repo for this website` }
>
<IoLogoGithub className={ `mt-1` } />
</Link>
</div>
);
};
export default Footer;
-168
View File
@@ -1,168 +0,0 @@
'use client';
import React, { useEffect, useState } from 'react';
import { Button } from '@nextui-org/react';
import Image from 'next/image';
import { 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';
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'
}
];
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() {
// const [ scrolled, setScrolled ] = useState(false);
// useEffect(() => {
// const handleScroll = () => {
// const isScrolled = window.scrollY > 0;
// setScrolled(isScrolled);
// };
// window.addEventListener('scroll', handleScroll);
// return () => {
// window.removeEventListener('scroll', handleScroll);
// };
// }, []);
const path = usePathname();
const [ shownPath, setShownPath ] = useState(path);
useEffect(() => {
let trimmed_path = path.split(`/`)[1];
if (trimmed_path === '') {
setShownPath('HOME');
} else {
setShownPath(trimmed_path.toUpperCase());
}
}, [ path ]);
const isMobile = useScreenSizeTrigger('lg');
return (
<>
<div
className={ `flex flex-col min-w-[50px] lg:min-w-[200px] ml-2 mr-0 lg:ml-8 lg:mr-6 min-h-screen pt-8 sticky` }>
<div className={ `fixed min-h-screen h-full` }>
<div className={ `hidden lg:block` }>
<div className={ `flex justify-center items-center my-7 h-[200px] w-[200px]` }>
<div className={ 'relative w-full h-full' }>
<Image
src={ 'https://oss.lumither.com/blog/pictures/avatar.webp' }
alt={ 'avatar' }
fill
className={ 'rounded-full object-cover' }
loading={ 'eager' }
></Image>
</div>
</div>
<div>
<p className={ `font-bold text-2xl` }>Lumither Tao</p>
<p className={ `text-xl text-zinc-500 dark:text-zinc-400` }>Ad Astra</p>
</div>
</div>
<div>
<ul className={ `flex flex-col space-y-2 lg:space-y-0 lg:flex-row items-center my-4 lg:space-x-2` }>
{
connections.map((conn, key) => (
<li key={ key }>
<Button as={ Link }
href={ conn.href }
aria-label={ conn.label }
variant={ `light` }
color={ `default` }
className={ `w-fit` }
isIconOnly
>
{ conn.icon }
</Button>
</li>
))
}
</ul>
</div>
<div
className={ `flex justify-center lg:justify-start w-full mt-8 ml-0 lg:-ml-3` }>
<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>
<div className={ `mt-8 flex lg:hidden justify-center w-full` }>
<p className={ `text-center sideways-lr font-bold text-2xl text-zinc-500 dark:text-zinc-400` }>
{ shownPath }
</p>
</div>
<div
className={ `absolute bottom-16` }
>
<ThemeSwitcher />
</div>
</div>
</div>
</>
);
}
export default Navbar;
-93
View File
@@ -1,93 +0,0 @@
'use client';
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';
const switcherElements = new Map(
[
[ 'dark', {
'display_name': 'Dark Mode',
'icon': <MdOutlineDarkMode />
} ],
[ 'light', {
'display_name': 'Light Mode',
'icon': <MdOutlineLightMode />
} ],
[ 'system', {
'display_name': 'Follow System',
'icon': <MdOutlineDesktopWindows />
} ]
]
);
function ThemeSwitcher() {
const [ mounted, setMounted ] = useState(false);
const { theme, setTheme } = useTheme();
const [ currTheme, setCurrTheme ] = useState(new Set([ theme as string ]));
const isMobile = useScreenSizeTrigger('lg');
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<div className={ `` }>
<Dropdown backdrop={ 'blur' }>
<DropdownTrigger>
<Button
variant={ 'flat' }
aria-label={ 'Theme Trigger' }
fullWidth
isIconOnly={ isMobile }
>
{ switcherElements.get(currTheme.values().next().value)?.icon }
<p className={ 'hidden lg:block' }>
{ switcherElements.get(currTheme.values().next().value)?.display_name }
</p>
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label={ 'Toggle Theme' }
variant={ 'flat' }
selectionMode={ 'single' }
selectedKeys={ currTheme }
onSelectionChange={ ({ anchorKey }) => {
setCurrTheme(new Set([ anchorKey as string ]));
setTheme(anchorKey as string);
} }
>
<DropdownItem
key={ 'light' }
startContent={ <MdOutlineLightMode /> }>
Light Mode
</DropdownItem>
<DropdownItem
key={ 'dark' }
startContent={ <MdOutlineDarkMode /> }
>
Dark Mode
</DropdownItem>
<DropdownItem
key={ 'system' }
startContent={ <MdOutlineDesktopWindows /> }
>
Follow System
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
}
export default ThemeSwitcher;
-18
View File
@@ -1,18 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'oss.lumither.com'
},
{
protocol: 'https',
hostname: 'pic.re'
}
]
}
// output: 'standalone'
};
export default nextConfig;
+7067
View File
File diff suppressed because it is too large. Load diff
+38 -36
View File
@@ -1,42 +1,44 @@
{
"name": "blog",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"name": "my-qwik-empty-starter",
"description": "Blank project with routing included",
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"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",
"next": "14.2.3",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
"remark-toc": "^9.0.0",
"sharp": "^0.33.4"
"engines-annotation": "Mostly required by sharp which needs a Node-API v9 compatible runtime",
"private": true,
"trustedDependencies": [
"sharp"
],
"trustedDependencies-annotation": "Needed for bun to allow running install scripts",
"type": "module",
"scripts": {
"build": "qwik build",
"build.client": "vite build",
"build.preview": "vite build --ssr src/entry.preview.tsx",
"build.types": "tsc --incremental --noEmit",
"deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'",
"dev": "vite --mode ssr",
"dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
"fmt": "prettier --write .",
"fmt.check": "prettier --check .",
"lint": "eslint \"src/**/*.ts*\"",
"preview": "qwik build preview && vite preview --open",
"start": "vite --open --mode ssr",
"qwik": "qwik"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.14",
"@types/node": "^20.14.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.2",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.9",
"typescript": "^5.5.4"
"@builder.io/qwik": "^1.12.0",
"@builder.io/qwik-city": "^1.12.0",
"@types/eslint": "8.56.10",
"@types/node": "20.14.11",
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"eslint": "8.57.0",
"eslint-plugin-qwik": "^1.12.0",
"prettier": "3.3.3",
"typescript": "5.4.5",
"undici": "*",
"vite": "5.3.5",
"vite-tsconfig-paths": "^4.2.1"
}
}
-9
View File
@@ -1,9 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500"><g clip-path="url(#a)"><circle cx="250" cy="250" r="250" fill="#fff"/><path fill="#18B6F6" d="m367.87 418.45-61.17-61.18-.94.13v-.67L175.7 227.53l32.05-31.13L188.9 87.73 99.56 199.09c-15.22 15.42-18.03 40.51-7.08 59.03l55.83 93.11a46.82 46.82 0 0 0 40.73 22.81l27.65-.27 151.18 44.68Z"/><path fill="#AC7EF4" d="m401.25 196.94-12.29-22.81-6.41-11.67-2.54-4.56-.26.26-33.66-58.63a47.07 47.07 0 0 0-41.27-23.75l-29.51.8-88.01.28a47.07 47.07 0 0 0-40.33 23.34L93.4 207l95.76-119.54L314.7 226.19l-22.3 22.67 13.35 108.54.13-.26v.26h-.26l.26.27 10.42 10.2 50.62 49.78c2.13 2 5.6-.4 4.13-2.96l-31.25-61.85 54.5-101.3 1.73-2c.67-.81 1.33-1.62 1.87-2.42a46.8 46.8 0 0 0 3.34-50.18Z"/><path fill="#fff" d="M315.1 225.65 189.18 87.6l17.9 108.14L175 227l130.5 130.27-11.75-108.14 21.37-23.48Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h500v500H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 947 B

+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "qwik-project-name",
"short_name": "Welcome to Qwik",
"start_url": ".",
"display": "standalone",
"background_color": "#fff",
"description": "A Qwik project app."
}
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
// This file can be used to add references for global types like `vite/client`.
// Add global `vite/client` types. For more info, see: https://vitejs.dev/guide/features#client-types
/// <reference types="vite/client" />
@@ -0,0 +1,48 @@
import { component$ } from "@builder.io/qwik";
import { useDocumentHead, useLocation } from "@builder.io/qwik-city";
/**
* The RouterHead component is placed inside of the document `<head>` element.
*/
export const RouterHead = component$(() => {
const head = useDocumentHead();
const loc = useLocation();
return (
<>
<title>{head.title}</title>
<link rel="canonical" href={loc.url.href} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
{head.meta.map((m) => (
<meta key={m.key} {...m} />
))}
{head.links.map((l) => (
<link key={l.key} {...l} />
))}
{head.styles.map((s) => (
<style
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.style })}
/>
))}
{head.scripts.map((s) => (
<script
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.script })}
/>
))}
</>
);
});
+17
View File
@@ -0,0 +1,17 @@
/*
* WHAT IS THIS FILE?
*
* Development entry point using only client-side modules:
* - Do not use this mode in production!
* - No SSR
* - No portion of the application is pre-rendered on the server.
* - All of the application is running eagerly in the browser.
* - More code is transferred to the browser than in SSR mode.
* - Optimizer/Serialization/Deserialization code is not exercised!
*/
import { render, type RenderOptions } from "@builder.io/qwik";
import Root from "./root";
export default function (opts: RenderOptions) {
return render(document, <Root />, opts);
}
+21
View File
@@ -0,0 +1,21 @@
/*
* WHAT IS THIS FILE?
*
* It's the bundle entry point for `npm run preview`.
* That is, serving your app built in production mode.
*
* Feel free to modify this file, but don't remove it!
*
* Learn more about Vite's preview command:
* - https://vitejs.dev/config/preview-options.html#preview-options
*
*/
import { createQwikCity } from "@builder.io/qwik-city/middleware/node";
import qwikCityPlan from "@qwik-city-plan";
// make sure qwikCityPlan is imported before entry
import render from "./entry.ssr";
/**
* The default export is the QwikCity adapter used by Vite preview.
*/
export default createQwikCity({ render, qwikCityPlan });
+33
View File
@@ -0,0 +1,33 @@
/**
* WHAT IS THIS FILE?
*
* SSR entry point, in all cases the application is rendered outside the browser, this
* entry point will be the common one.
*
* - Server (express, cloudflare...)
* - npm run start
* - npm run preview
* - npm run build
*
*/
import {
renderToStream,
type RenderToStreamOptions,
} from "@builder.io/qwik/server";
import { manifest } from "@qwik-client-manifest";
import Root from "./root";
export default function (opts: RenderToStreamOptions) {
return renderToStream(<Root />, {
manifest,
...opts,
// Use container attributes to set attributes on the html tag.
containerAttributes: {
lang: "en-us",
...opts.containerAttributes,
},
serverData: {
...opts.serverData,
},
});
}
View File
Whitespace-only changes.
+38
View File
@@ -0,0 +1,38 @@
import { component$ } from "@builder.io/qwik";
import {
QwikCityProvider,
RouterOutlet,
ServiceWorkerRegister,
} from "@builder.io/qwik-city";
import { RouterHead } from "./components/router-head/router-head";
import { isDev } from "@builder.io/qwik";
import "./global.css";
export default component$(() => {
/**
* The root of a QwikCity site always start with the <QwikCityProvider> component,
* immediately followed by the document's <head> and <body>.
*
* Don't remove the `<head>` and `<body>` elements.
*/
return (
<QwikCityProvider>
<head>
<meta charset="utf-8" />
{!isDev && (
<link
rel="manifest"
href={`${import.meta.env.BASE_URL}manifest.json`}
/>
)}
<RouterHead />
</head>
<body lang="en">
<RouterOutlet />
{!isDev && <ServiceWorkerRegister />}
</body>
</QwikCityProvider>
);
});
+25
View File
@@ -0,0 +1,25 @@
import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
export default component$(() => {
return (
<>
<h1>Hi 👋</h1>
<div>
Can't wait to see what you build with qwik!
<br />
Happy coding.
</div>
</>
);
});
export const head: DocumentHead = {
title: "Welcome to Qwik",
meta: [
{
name: "description",
content: "Qwik site description",
},
],
};
+17
View File
@@ -0,0 +1,17 @@
import { component$, Slot } from "@builder.io/qwik";
import type { RequestHandler } from "@builder.io/qwik-city";
export const onGet: RequestHandler = async ({ cacheControl }) => {
// Control caching for this request for best performance and to reduce hosting costs:
// https://qwik.dev/docs/caching/
cacheControl({
// Always serve a cached response by default, up to a week stale
staleWhileRevalidate: 60 * 60 * 24 * 7,
// Max once every 5 seconds, revalidate on the server to get a fresh version of this page
maxAge: 5,
});
};
export default component$(() => {
return <Slot />;
});
+18
View File
@@ -0,0 +1,18 @@
/*
* WHAT IS THIS FILE?
*
* The service-worker.ts file is used to have state of the art prefetching.
* https://qwik.dev/qwikcity/prefetching/overview/
*
* Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline.
* You can also use this file to add more functionality that runs in the service worker.
*/
import { setupServiceWorker } from "@builder.io/qwik-city/service-worker";
setupServiceWorker();
addEventListener("install", () => self.skipWaiting());
addEventListener("activate", () => self.clients.claim());
declare const self: ServiceWorkerGlobalScope;
-60
View File
@@ -1,60 +0,0 @@
import { nextui } from '@nextui-org/react';
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
],
// theme: {
// extend: {}
// },
darkMode: 'class',
plugins: [
require('@tailwindcss/typography'),
nextui(
{
// defaultTheme: 'dark',
defaultExtendTheme: 'light',
themes: {
'light': {
layout: {},
colors: {}
},
'dark': {
layout: {},
colors: {}
}
}
}
),
plugin(function ({ addUtilities, addComponents, e, config }) {
addUtilities(
{
'.horizontal-tb': {
writingMode: 'horizontal-tb'
},
'.vertical-rl': {
writingMode: 'vertical-rl'
},
'.vertical-lr': {
writingMode: 'vertical-lr'
},
'.sideways-lr': {
writingMode: 'vertical-lr',
transform: 'rotate(180deg)'
},
'.sideways-rl': {
writingMode: 'sideways-rl',
transform: 'rotate(180deg)'
}
}
);
})
]
};
export default config;
+15 -17
View File
@@ -1,27 +1,25 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"target": "ES2017",
"module": "ES2022",
"lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
"jsx": "react-jsx",
"jsxImportSource": "@builder.io/qwik",
"strict": true,
"target": "ESNext",
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"isolatedModules": true,
"outDir": "tmp",
"noEmit": true,
"paths": {
"@/*": ["./*"]
"~/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"files": ["./.eslintrc.cjs"],
"include": ["src", "./*.d.ts", "./*.config.ts"]
}
+106
View File
@@ -0,0 +1,106 @@
/**
* This is the base config for vite.
* When building, the adapter config is used which loads this file and extends it.
*/
import { defineConfig, type UserConfig } from "vite";
import { qwikVite } from "@builder.io/qwik/optimizer";
import { qwikCity } from "@builder.io/qwik-city/vite";
import tsconfigPaths from "vite-tsconfig-paths";
import pkg from "./package.json";
type PkgDep = Record<string, string>;
const { dependencies = {}, devDependencies = {} } = pkg as any as {
dependencies: PkgDep;
devDependencies: PkgDep;
[key: string]: unknown;
};
errorOnDuplicatesPkgDeps(devDependencies, dependencies);
/**
* Note that Vite normally starts from `index.html` but the qwikCity plugin makes start at `src/entry.ssr.tsx` instead.
*/
export default defineConfig(({ command, mode }): UserConfig => {
return {
plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
// This tells Vite which dependencies to pre-build in dev mode.
optimizeDeps: {
// Put problematic deps that break bundling here, mostly those with binaries.
// For example ['better-sqlite3'] if you use that in server functions.
exclude: [],
},
/**
* This is an advanced setting. It improves the bundling of your server code. To use it, make sure you understand when your consumed packages are dependencies or dev dependencies. (otherwise things will break in production)
*/
// ssr:
// command === "build" && mode === "production"
// ? {
// // All dev dependencies should be bundled in the server build
// noExternal: Object.keys(devDependencies),
// // Anything marked as a dependency will not be bundled
// // These should only be production binary deps (including deps of deps), CLI deps, and their module graph
// // If a dep-of-dep needs to be external, add it here
// // For example, if something uses `bcrypt` but you don't have it as a dep, you can write
// // external: [...Object.keys(dependencies), 'bcrypt']
// external: Object.keys(dependencies),
// }
// : undefined,
server: {
headers: {
// Don't cache the server response in dev mode
"Cache-Control": "public, max-age=0",
},
},
preview: {
headers: {
// Do cache the server response in preview (non-adapter production build)
"Cache-Control": "public, max-age=600",
},
},
};
});
// *** utils ***
/**
* Function to identify duplicate dependencies and throw an error
* @param {Object} devDependencies - List of development dependencies
* @param {Object} dependencies - List of production dependencies
*/
function errorOnDuplicatesPkgDeps(
devDependencies: PkgDep,
dependencies: PkgDep,
) {
let msg = "";
// Create an array 'duplicateDeps' by filtering devDependencies.
// If a dependency also exists in dependencies, it is considered a duplicate.
const duplicateDeps = Object.keys(devDependencies).filter(
(dep) => dependencies[dep],
);
// include any known qwik packages
const qwikPkg = Object.keys(dependencies).filter((value) =>
/qwik/i.test(value),
);
// any errors for missing "qwik-city-plan"
// [PLUGIN_ERROR]: Invalid module "@qwik-city-plan" is not a valid package
msg = `Move qwik packages ${qwikPkg.join(", ")} to devDependencies`;
if (qwikPkg.length > 0) {
throw new Error(msg);
}
// Format the error message with the duplicates list.
// The `join` function is used to represent the elements of the 'duplicateDeps' array as a comma-separated string.
msg = `
Warning: The dependency "${duplicateDeps.join(", ")}" is listed in both "devDependencies" and "dependencies".
Please move the duplicated dependencies to "devDependencies" only and remove it from "dependencies"
`;
// Throw an error with the constructed message.
if (duplicateDeps.length > 0) {
throw new Error(msg);
}
}
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more