diff --git a/backend/src/api/get/get_page_count.rs b/backend/src/api/get/get_page_count.rs new file mode 100644 index 0000000..887e360 --- /dev/null +++ b/backend/src/api/get/get_page_count.rs @@ -0,0 +1,25 @@ +use crate::constants::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, +) -> Result, Json> { + 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()}))) + } + } +} diff --git a/backend/src/api/get/get_post_list.rs b/backend/src/api/get/get_post_list.rs index 74307cb..1b55610 100644 --- a/backend/src/api/get/get_post_list.rs +++ b/backend/src/api/get/get_post_list.rs @@ -1,9 +1,10 @@ +use crate::constants::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::{PgPool, postgres::PgRow, query, Row}; +use sqlx::{postgres::PgRow, query, PgPool, Row}; use tracing::error; use uuid::Uuid; @@ -12,13 +13,19 @@ pub struct Params { page: Option, } -const PAGE_SIZE: u32 = 6; - pub async fn get_post_list( State(db_connection): State, Query(params): Query, ) -> Result, (StatusCode, Json)> { - let offset = PAGE_SIZE * (params.page.unwrap_or(1) - 1); + 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) @@ -34,7 +41,7 @@ pub async fn get_post_list( ); return Err(( StatusCode::INTERNAL_SERVER_ERROR, - Json::from(json!(("error", e.to_string()))), + Json::from(json!({"error": e.to_string()})), )); } }; diff --git a/backend/src/api/get/mod.rs b/backend/src/api/get/mod.rs index a30b1d7..5e23c3d 100644 --- a/backend/src/api/get/mod.rs +++ b/backend/src/api/get/mod.rs @@ -1,2 +1,3 @@ +pub mod get_page_count; pub mod get_post; pub mod get_post_list; diff --git a/backend/src/constants.rs b/backend/src/constants.rs index 1206c64..5dd57e4 100644 --- a/backend/src/constants.rs +++ b/backend/src/constants.rs @@ -1 +1,3 @@ pub const GLOBAL_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z"; + +pub const PAGE_SIZE: u32 = 6; diff --git a/backend/src/main.rs b/backend/src/main.rs index fa2f24a..eaf806d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -4,16 +4,16 @@ use std::num::ParseIntError; use std::process::exit; use std::str::FromStr; -use axum::Router; -use axum::routing::{get, post}; -use dotenv::dotenv; -use sqlx::ConnectOptions; -use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use tracing::{error, info, warn}; - +use crate::api::get::get_page_count::get_page_count; use crate::api::get::get_post::get_post; use crate::api::get::get_post_list::get_post_list; use crate::api::post::refresh::refresh; +use axum::routing::{get, post}; +use axum::Router; +use dotenv::dotenv; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use sqlx::ConnectOptions; +use tracing::{error, info, warn}; mod api; mod constants; @@ -23,6 +23,16 @@ mod model; #[tokio::main] async fn main() -> Result<(), Box> { + if let Some(env_file) = env::args().nth(1) { + match dotenv::from_filename(&env_file) { + Ok(_) => { + println!("[Info] env loaded from {}", env_file); + } + Err(e) => { + panic!("[Fatal] failed to read {}: {}", env_file, e); + } + } + }; match dotenv() { Ok(_) => { println!("[Info] env loaded from .env file, starting up...") @@ -42,11 +52,7 @@ async fn main() -> Result<(), Box> { Err(_) => 3001, }; - let db_uri: String = env::var("DB_URI").unwrap_or_else(|e| { - error!("Invalid or missing env var: DB_URI: {}", e.to_string()); - exit(1); - }); - let db_connect_option = PgConnectOptions::from_str(&db_uri) + let db_connect_option = PgConnectOptions::from_str(dbg!(&parse_db_uri())) .unwrap() .disable_statement_logging(); @@ -64,6 +70,7 @@ async fn main() -> Result<(), Box> { let app = 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)) .with_state(db_pool); @@ -81,3 +88,15 @@ async fn main() -> Result<(), Box> { Ok(()) } + +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}") +} diff --git a/frontend/app/blog/Pagination.tsx b/frontend/app/blog/Pagination.tsx new file mode 100644 index 0000000..6b53dac --- /dev/null +++ b/frontend/app/blog/Pagination.tsx @@ -0,0 +1,52 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Pagination as NextUIPagination } from '@nextui-org/react'; +import { useRouter, useSearchParams } from 'next/navigation'; + +const Pagination = ({ total }: { + total: number, +}) => { + const parsePage = Number(useSearchParams().get('page')); + const currPage = parsePage > 0 ? parsePage : 1; + + const router = useRouter(); + + const maxMobileWidth: number = 768; + const [ isMobile, setIsMobile ] = useState(false); + useEffect(() => { + const updateMobileWidth = () => { + if (window.innerWidth <= maxMobileWidth) { + setIsMobile(true); + } else { + setIsMobile(false); + } + }; + updateMobileWidth(); + window.addEventListener('resize', updateMobileWidth); + return () => { + window.removeEventListener('resize', updateMobileWidth); + }; + + }, []); + + return ( +
+ { + router.push(`?page=${ page }`, { scroll: false }); + } } + /> +
+ + ); +}; +export default Pagination; diff --git a/frontend/app/blog/PostList.tsx b/frontend/app/blog/PostList.tsx new file mode 100644 index 0000000..4eff3a8 --- /dev/null +++ b/frontend/app/blog/PostList.tsx @@ -0,0 +1,42 @@ +'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 +
+
    + { posts.map((post: JSON, key: number) => ( +
  • + + + +
  • ) + ) } +
+ +
+ ); + } catch (e: any) { + return ( + // todo: use Error.tsx + + + Fatal: { e.toString() } + + ); + } +}; +export default PostList; diff --git a/frontend/app/blog/api.ts b/frontend/app/blog/api.ts index 307316a..9b4c73e 100644 --- a/frontend/app/blog/api.ts +++ b/frontend/app/blog/api.ts @@ -13,15 +13,9 @@ const NOCTURNE_ENDPOINT = ((isServerSide = true) => { } }); -export async function fetchNocturneApi(path: string, isServerSideRequest = true): Promise<{ code: number, ret: any }> { - let res = await fetch(`${ NOCTURNE_ENDPOINT(isServerSideRequest) }${ path }`, { +export async function fetchNocturneApi(path: string, isServerSideRequest = true) { + return await fetch(`${ NOCTURNE_ENDPOINT(isServerSideRequest) }${ path }`, { cache: 'no-cache' }); - - if (!res.ok) { - return { code: res.status, ret: (await res.json()) }; - } - - return { code: res.status, ret: await res.json() }; } diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx index b2fe73c..808192d 100644 --- a/frontend/app/blog/page.tsx +++ b/frontend/app/blog/page.tsx @@ -1,39 +1,26 @@ -import React from 'react'; -import BlogPostCard from '@/app/blog/BlogPostCard'; -import { Card, CardBody } from '@nextui-org/card'; -import { MotionDiv } from '@/app/public/MotionDiv'; +import PostList from '@/app/blog/PostList'; +import Pagination from '@/app/blog/Pagination'; import { fetchNocturneApi } from '@/app/blog/api'; -async function Blog() { +async function Blog({ searchParams }: { + searchParams?: { + page?: number + }; +}) { - const { code, ret } = await fetchNocturneApi('/get_post_list'); - if (code == 200) { - let posts = ret['posts']; - return ( - // blog list -
    - { posts.map((post: JSON, key: number) => ( -
  • - - - -
  • ) - ) } -
- ); - } else { - return ( - // todo: use Error.tsx - - - Fatal: { ret } - - ); - } + 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; + + console.log(currPage); + + return ( +
+ + +
+ ); } export default Blog; diff --git a/frontend/app/blog/post/[id]/MinifyPostCard.tsx b/frontend/app/blog/post/[id]/MinifyPostCard.tsx index bf96ee4..20f6925 100644 --- a/frontend/app/blog/post/[id]/MinifyPostCard.tsx +++ b/frontend/app/blog/post/[id]/MinifyPostCard.tsx @@ -23,13 +23,15 @@ const MinifyPostCard = (props: Props) => {

{ desc }

{ img_url && -
- { +
+
+ { +
diff --git a/frontend/app/blog/post/[id]/PostCard.tsx b/frontend/app/blog/post/[id]/PostCard.tsx index b64b94d..817c28e 100644 --- a/frontend/app/blog/post/[id]/PostCard.tsx +++ b/frontend/app/blog/post/[id]/PostCard.tsx @@ -17,11 +17,11 @@ type Props = { } async function PostCard(props: Props) { - - const { code, ret } = await fetchNocturneApi(`/get_post/${ props.id }`); - if (code == 200) { + try { + const res = await fetchNocturneApi(`/get_post/${ props.id }`); + const ret: any = await res.json(); return (
@@ -85,11 +85,11 @@ async function PostCard(props: Props) {
); - } else { + } catch (e: any) { return
- Fatal: { ret } + Fatal: { e.toString() }
; diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index 9d72d3b..cfa2e53 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -89,11 +89,11 @@ function Navbar() { }, [ path ]); - const minMobileWidth: number = 1024; + const maxMobileWidth: number = 1024; const [ isMobile, setIsMobile ] = useState(false); useEffect(() => { const updateMobileWidth = () => { - if (window.innerWidth <= minMobileWidth) { + if (window.innerWidth <= maxMobileWidth) { setIsMobile(true); } else { setIsMobile(false); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index c98ab2c..1ff5b8e 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -11,8 +11,8 @@ const nextConfig = { hostname: 'pic.re' } ] - }, - output: 'standalone' + } + // output: 'standalone' }; export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json index 580b7b0..ed666ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,15 +9,15 @@ "lint": "next lint" }, "dependencies": { - "@nextui-org/react": "^2.3.5", + "@nextui-org/react": "^2.4.6", "@types/react-syntax-highlighter": "^15.5.13", - "framer-motion": "^11.2.10", + "framer-motion": "^11.3.24", "lxgw-wenkai-webfont": "^1.7.0", "next": "14.2.3", "next-themes": "^0.3.0", - "react": "^18", - "react-dom": "^18", - "react-icons": "^5.1.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", @@ -28,15 +28,15 @@ "sharp": "^0.33.4" }, "devDependencies": { - "@tailwindcss/typography": "^0.5.13", - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.4.19", - "eslint": "^8", + "@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", - "tailwindcss": "^3.4.1", - "typescript": "^5" + "postcss": "^8.4.41", + "tailwindcss": "^3.4.9", + "typescript": "^5.5.4" } }