diff --git a/frontend/app/(pages)/blog/BlogPostCard.tsx b/frontend/app/(pages)/blog/BlogPostCard.tsx index 1d49e9e..66eb703 100644 --- a/frontend/app/(pages)/blog/BlogPostCard.tsx +++ b/frontend/app/(pages)/blog/BlogPostCard.tsx @@ -4,6 +4,8 @@ import { Chip } from '@nextui-org/react'; import Image from 'next/image'; import Link from 'next/link'; import { MdCalendarMonth, MdNorthEast } from 'react-icons/md'; +import { HighlightSelector } from '@/app/(pages)/blog/post/components/AnimationPostList'; +import TagChip from '@/app/(pages)/blog/TagChip'; interface Post { id: string, @@ -19,15 +21,20 @@ interface Post { type Props = { post: Post + + highlightSelector: HighlightSelector, } -function BlogPostCard({ post }: Props) { +function BlogPostCard({ post, highlightSelector }: Props) { + const highlightTags = highlightSelector?.tags; + return (
- { post.header_img &&
@@ -63,12 +70,26 @@ function BlogPostCard({ post }: Props) {
    - { - post.tags.map((tag: string, key: any) => ( -
  • - { tag } -
  • - )) + {/*highlight*/ + post.tags + .filter((tag) => highlightTags?.includes(tag)) + .sort() + .map((tag: string, key: any) => ( +
  • + +
  • + )) + } + + {/*normal */ + post.tags + .filter((tag) => !highlightTags?.includes(tag)) + .sort() + .map((tag: string, key: any) => ( +
  • + +
  • + )) }
diff --git a/frontend/app/(pages)/blog/Pagination.tsx b/frontend/app/(pages)/blog/Pagination.tsx index 3d381ab..2216726 100644 --- a/frontend/app/(pages)/blog/Pagination.tsx +++ b/frontend/app/(pages)/blog/Pagination.tsx @@ -8,7 +8,9 @@ import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger'; const Pagination = ({ total }: { total: number, }) => { - const parsePage = Number(useSearchParams().get('page')); + const searchParams = useSearchParams(); + + const parsePage = Number(searchParams.get('page')); const currPage = parsePage > 0 ? parsePage : 1; const router = useRouter(); @@ -27,7 +29,10 @@ const Pagination = ({ total }: { color={ 'default' } variant={ 'light' } onChange={ (page) => { - router.push(`?page=${ page }`, { scroll: false }); + const params = new URLSearchParams(searchParams.toString()); + params.set('page', page.toString()); + const queryString = params.toString(); + router.push(`?${ queryString }`, { scroll: false }); } } />
diff --git a/frontend/app/(pages)/blog/PostList.tsx b/frontend/app/(pages)/blog/PostList.tsx index fa9f234..abc69be 100644 --- a/frontend/app/(pages)/blog/PostList.tsx +++ b/frontend/app/(pages)/blog/PostList.tsx @@ -1,19 +1,19 @@ -'use server'; - import React from 'react'; -import { fetchPostList } from '@/src/api/blog/post'; -import AnimationPostList from '@/app/(pages)/blog/post/components/AnimationPostList'; +import { Post } from '@/src/api/blog/post'; +import AnimationPostList, { HighlightSelector } from '@/app/(pages)/blog/post/components/AnimationPostList'; -const PostList = async ({ page }: { page: number }) => { +interface Props { + posts: Post[], + tags: string[] | undefined +} + +const PostList = async ({ posts, tags }: Props) => { try { - const postListResponse = await fetchPostList(page); - - const posts = postListResponse.data.posts; - return ( - // blog list
- +
); } catch (e: any) { diff --git a/frontend/app/(pages)/blog/TagChip.tsx b/frontend/app/(pages)/blog/TagChip.tsx new file mode 100644 index 0000000..874d8fe --- /dev/null +++ b/frontend/app/(pages)/blog/TagChip.tsx @@ -0,0 +1,48 @@ +'use client'; + +import React from 'react'; +import { Chip } from '@nextui-org/react'; +import { useRouter, useSearchParams } from 'next/navigation'; + +interface TagChipProps { + tagName: string, + isHighlighted: boolean +} + +const TagChip = ({ tagName, isHighlighted }: TagChipProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const toggleTag = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const params = new URLSearchParams(searchParams.toString()); + const tags = params.getAll('tags'); + + if (tags.includes(tagName)) { + const newTags = tags.filter(t => t !== tagName); + params.delete('tags'); + newTags.forEach(t => params.append('tags', t)); + } else { + params.append('tags', tagName); + } + + const queryString = params.toString(); + router.push(`?${ queryString }`, { scroll: false }); + }; + return ( +
+ { tagName } +
+ ); +}; + +export default TagChip; \ No newline at end of file diff --git a/frontend/app/(pages)/blog/ToolBar.tsx b/frontend/app/(pages)/blog/ToolBar.tsx new file mode 100644 index 0000000..b67df1f --- /dev/null +++ b/frontend/app/(pages)/blog/ToolBar.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const ToolBar = () => { + return ( +
+ +
+ ); +}; + +export default ToolBar; \ No newline at end of file diff --git a/frontend/app/(pages)/blog/page.tsx b/frontend/app/(pages)/blog/page.tsx index a544c74..bddb92a 100644 --- a/frontend/app/(pages)/blog/page.tsx +++ b/frontend/app/(pages)/blog/page.tsx @@ -1,6 +1,7 @@ import PostList from '@/app/(pages)/blog/PostList'; import Pagination from '@/app/(pages)/blog/Pagination'; -import { fetchPostListPagination } from '@/src/api/blog/post'; +import { fetchPostList } from '@/src/api/blog/post'; +import ToolBar from '@/app/(pages)/blog/ToolBar'; async function Blog({ @@ -9,18 +10,40 @@ async function Blog({ searchParams: Promise<{ [_: string]: string | string[] | undefined }> }) { - const parsePage = Number((await searchParams)?.page); + const sp = await searchParams; + + const parsePage = Number(sp?.page); const currPage = parsePage > 0 ? parsePage : 1; + const matchAll = Boolean(sp?.match_all); + + const ensureList = (input: string | string[] | undefined): string[] | undefined => { + if (!input) { + return undefined; + } + if (typeof input === 'string') { + return [ input ]; + } else { + return input; + } + }; + const tags = ensureList(sp?.tags); try { - const pagination = await fetchPostListPagination(); - const pageCount = pagination.data.page_count; + const postListResponse = await fetchPostList({ + page: currPage, + match_all: matchAll, + tag_filter: tags + }); + + const pagination = postListResponse.data.pagination; + const posts = postListResponse.data.posts; return (
- - + + +
); } catch (e: any) { @@ -28,7 +51,7 @@ async function Blog({

- { 'Unexpected Error [loading pagination]:' } + { 'Unexpected Error [loading blog posts]:' }

{ e.message } diff --git a/frontend/app/(pages)/blog/post/[id]/Headers.tsx b/frontend/app/(pages)/blog/post/[id]/Headers.tsx index a295173..6564636 100644 --- a/frontend/app/(pages)/blog/post/[id]/Headers.tsx +++ b/frontend/app/(pages)/blog/post/[id]/Headers.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Image from 'next/image'; import { Chip } from '@nextui-org/react'; import { MdCalendarMonth } from 'react-icons/md'; +import TagChip from '@/app/(pages)/blog/post/[id]/TagChip'; type Props = { title: string; @@ -50,7 +51,7 @@ const Headers = (post: Props) => { { post.tags.map((tag: string, key: any) => (

  • - { tag } +
  • )) } diff --git a/frontend/app/(pages)/blog/post/[id]/TagChip.tsx b/frontend/app/(pages)/blog/post/[id]/TagChip.tsx new file mode 100644 index 0000000..fa48a8d --- /dev/null +++ b/frontend/app/(pages)/blog/post/[id]/TagChip.tsx @@ -0,0 +1,20 @@ +'use client'; + +import React from 'react'; +import { Chip } from '@nextui-org/react'; +import { useRouter } from 'next/navigation'; + +const TagChip = ({ tag }: { tag: string }) => { + const router = useRouter(); + + return ( +
    router.push(`/blog?tags=${ tag }`) }> + { tag } +
    + ); +}; + +export default TagChip; \ No newline at end of file diff --git a/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx b/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx index b5dd424..2019179 100644 --- a/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx +++ b/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx @@ -7,7 +7,16 @@ import * as motion from 'motion/react-client'; import { AnimatePresence } from 'motion/react'; import { Post } from '@/src/api/blog/post'; -const PostList = ({ posts }: { posts: Post[] }) => { +export interface HighlightSelector { + tags?: string[]; +} + +interface Props { + posts: Post[]; + highlight: HighlightSelector; +} + +const PostList = ({ posts, highlight }: Props) => { return (
      @@ -19,7 +28,7 @@ const PostList = ({ posts }: { posts: Post[] }) => { transition={ { ease: 'easeOut', duration: 0.5, delay: key * .1 + .1 } } key={ key } className={ 'w-full' }> - + ) ) } diff --git a/frontend/app/(pages)/layout.tsx b/frontend/app/(pages)/layout.tsx index 61f980a..c070318 100644 --- a/frontend/app/(pages)/layout.tsx +++ b/frontend/app/(pages)/layout.tsx @@ -14,7 +14,7 @@ function Layout({
      diff --git a/frontend/app/(pages)/public/screenSizeTrigger.ts b/frontend/app/(pages)/public/screenSizeTrigger.ts index 7a25074..225b598 100644 --- a/frontend/app/(pages)/public/screenSizeTrigger.ts +++ b/frontend/app/(pages)/public/screenSizeTrigger.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; const screenSize = new Map( [ + [ 'xs', 480 ], [ 'sm', 640 ], [ 'md', 768 ], [ 'lg', 1024 ], diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index e2fdce6..04bb4b9 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -21,9 +21,6 @@ export default function RootLayout({ -
      -

      *System Under Construction*

      -
      { children }
      diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index fa2b5de..adaed5c 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,63 +5,79 @@ import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger'; import { Button } from '@nextui-org/react'; import { entries } from '@/app/entries'; import Link from 'next/link'; -import BoxesGL from '@/components/BackgroundBoxGL'; -import { BoxesCore } from '@/components/BackgroundBox'; +import * as motion from 'motion/react-client'; +import { cn } from '@/src/utils'; export default function Home() { - const isMobile = useScreenSizeTrigger('sm'); + const isSm = useScreenSizeTrigger('sm'); + const isXs = useScreenSizeTrigger('xs'); return (
      - {/**/ } - - -
      -

      Atelier Lumiaethor

      -
      - -
      -
      - className={ 'h-full w-full rounded-3xl backdrop-blur align-middle grid grid-cols-2 content-center' }> +
      +

      Atelier Lumiaethor

      +

      In pursuit of absolute + simplicity and ultimate + sophistication

      +
      -
      -
      - test +
      +
      + + { !isSm && +
      +
      +

      + © 2024-{ new Date().getFullYear() } Lumither Tao +

      +

      Powered by Next.js and Rust, built with passion and love

      +

      LMTCloud ecosystem

      +
      +
      + } + +
      +
        + { + entries.map((meta, key) => ( +
      • + +
      • + )) + } +
      -
      -
      -
      -
        - { - entries.map((meta, key) => ( -
      • - -
      • - )) - } -
      -
      +
      ); } diff --git a/frontend/components/BackgroundBox.tsx b/frontend/components/BackgroundBox.tsx deleted file mode 100644 index 295074f..0000000 --- a/frontend/components/BackgroundBox.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; -import React from 'react'; -import { motion } from 'motion/react'; -import { cn } from '@/src/utils'; - -export const BoxesCore = ({ className, ...rest }: { className?: string }) => { - const rows = new Array(250).fill(1); - const cols = new Array(200).fill(1); - let colors = [ - '#93c5fd', - '#f9a8d4', - '#86efac', - '#fde047', - '#fca5a5', - '#d8b4fe', - '#93c5fd', - '#a5b4fc', - '#c4b5fd' - ]; - const getRandomColor = () => { - return colors[Math.floor(Math.random() * colors.length)]; - }; - - return ( -
      - { rows.map((_, i) => ( - - { cols.map((_, j) => ( - - { j % 2 === 0 && i % 2 === 0 ? ( - - - - ) : null } - - )) } - - )) } -
      - ); -}; - -export const Boxes = React.memo(BoxesCore); diff --git a/frontend/components/BackgroundBoxGL.tsx b/frontend/components/BackgroundBoxGL.tsx deleted file mode 100644 index 7f26ed0..0000000 --- a/frontend/components/BackgroundBoxGL.tsx +++ /dev/null @@ -1,266 +0,0 @@ -'use client'; -import React, { useEffect, useRef } from 'react'; - -// WebGL-based rewrite of the grid/hover effect originally implemented with many DOM nodes. -// Key design goals: -// - zero per-cell DOM; a single full-screen draw with a fragment shader -// - hover color derived deterministically from a small palette -// - optional CSS transform to match the original perspective/skew -// - high-DPI aware; resizes with container - -export default function BoxesGL({ - className, - style, - rows = 250, - cols = 200, - lineWidth = 1.0, // device pixels - transformLikeOriginal = true -}: { - className?: string - style?: React.CSSProperties - rows?: number - cols?: number - lineWidth?: number - transformLikeOriginal?: boolean -}) { - const canvasRef = useRef(null); - const rafRef = useRef(null); - const mouseRef = useRef<{ x: number; y: number } | null>(null); - - useEffect(() => { - const canvas = canvasRef.current!; - const gl = canvas.getContext('webgl2', { antialias: false, premultipliedAlpha: true }); - if (!gl) { - console.warn('WebGL2 not available'); - // Fail closed; nothing to do if WebGL2 is unavailable - return; - } - - // Create program (fullscreen triangle) - const vs = gl.createShader(gl.VERTEX_SHADER)!; - gl.shaderSource( - vs, - `#version 300 es\n - const vec2 verts[3] = vec2[3]( - vec2(-1.0, -1.0), - vec2(3.0, -1.0), - vec2(-1.0, 3.0) - ); - void main(){ - gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); - } - ` - ); - gl.compileShader(vs); - - const fs = gl.createShader(gl.FRAGMENT_SHADER)!; - gl.shaderSource( - fs, - `#version 300 es\n - precision highp float; - out vec4 o; - - uniform vec2 u_resolution; // canvas size in device pixels - uniform vec2 u_mouse; // mouse in device pixels, (-1,-1) if outside - uniform float u_time; // seconds - uniform float u_rows; - uniform float u_cols; - uniform float u_lineWidth; // in device pixels - - // Border color and plus sign color (matches tailwind slate-700-ish) - const vec3 BORDER = vec3(0.22, 0.27, 0.33); - const vec3 PLUS = vec3(0.22, 0.27, 0.33); - - // A small pastel palette like the original - vec3 palette(int idx){ - // 9-color palette - if(idx==0) return vec3(0.576,0.773,0.992); - if(idx==1) return vec3(0.976,0.659,0.831); - if(idx==2) return vec3(0.525,0.937,0.675); - if(idx==3) return vec3(0.992,0.878,0.278); - if(idx==4) return vec3(0.988,0.648,0.648); - if(idx==5) return vec3(0.847,0.706,0.996); - if(idx==6) return vec3(0.576,0.773,0.992); - if(idx==7) return vec3(0.647,0.706,0.988); - return vec3(0.769,0.710,0.992); - } - - // Hash helpers for stable per-cell randomness - float hash11(float x){ - x = fract(x * 0.1031); - x *= x + 33.33; - return fract(1e4 * x * (x + 0.33)); - } - float hash21(vec2 p){ - vec3 p3 = fract(vec3(p.xyx) * 0.1031); - p3 += dot(p3, p3.yzx + 33.33); - return fract((p3.x + p3.y) * p3.z); - } - - void main(){ - vec2 frag = gl_FragCoord.xy; // device pixels - vec2 gridSize = vec2(u_cols, u_rows); - vec2 cellSize = u_resolution / gridSize; // pixels per cell - - // Compute integer cell coordinates and local position within cell [0,1) - vec2 uvGrid = frag / u_resolution * gridSize; - vec2 cellId = floor(uvGrid); - vec2 cellUV = fract(uvGrid); - - // Grid lines: draw top+right borders to mimic original (border-t, border-r, border-l on first col) - float lw = u_lineWidth / max(cellSize.x, 1.0); // convert to relative width in [0,1] axis - float lwY = u_lineWidth / max(cellSize.y, 1.0); - - float lineTop = step(1.0 - lwY, cellUV.y); - float lineRight= step(1.0 - lw , cellUV.x); - float lineLeft = step(cellUV.x, lw) * step(0.5, step(0.0, cellId.x)); // left border for all cols via small bias - - float lineMask = max(lineTop, max(lineRight, lineLeft)); - - // Determine hovered cell index - vec2 hoverCell = vec2(-1.0); - bool hovering = u_mouse.x >= 0.0 && u_mouse.y >= 0.0; - if(hovering){ - vec2 hoverUVGrid = u_mouse / u_resolution * gridSize; - hoverCell = floor(hoverUVGrid); - } - bool isHover = hovering && all(equal(hoverCell, cellId)); - - // Background: transparent unless hovered. On hover, fill with palette color chosen by cell hash. - vec3 bg = vec3(0.0); - float alpha = 0.0; - if(isHover){ - // Stable color per cell, with slight time wobble to avoid banding on long hovers - float h = hash21(cellId + vec2(floor(u_time*0.25))); - int idx = int(floor(h * 9.0)) % 9; - bg = palette(idx); - alpha = 1.0; - } - - // Plus sign for even-even cells: thin cross centered - bool evenEven = mod(cellId.x, 2.0) < 0.5 && mod(cellId.y, 2.0) < 0.5; - float plusThicknessX = 0.5 * u_lineWidth / max(cellSize.x, 1.0); - float plusThicknessY = 0.5 * u_lineWidth / max(cellSize.y, 1.0); - float cross = 0.0; - if(evenEven){ - float vert = step(0.5 - plusThicknessX, abs(cellUV.x - 0.5)); - float hori = step(0.5 - plusThicknessY, abs(cellUV.y - 0.5)); - cross = 1.0 - max(vert, hori); - } - - vec3 color = mix(bg, BORDER, max(lineMask, 0.0)); - color = mix(color, PLUS, cross); - float a = max(alpha, max(lineMask, cross)); - - o = vec4(color, a); - } - ` - ); - gl.compileShader(fs); - - const prog = gl.createProgram()!; - gl.attachShader(prog, vs); - gl.attachShader(prog, fs); - gl.linkProgram(prog); - - // Check compile/link for diagnostics (silent in prod; could surface to console in dev) - if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { - console.warn('WebGL program link error:', gl.getProgramInfoLog(prog)); - } - - gl.useProgram(prog); - - const uResolution = gl.getUniformLocation(prog, 'u_resolution'); - const uMouse = gl.getUniformLocation(prog, 'u_mouse'); - const uTime = gl.getUniformLocation(prog, 'u_time'); - const uRows = gl.getUniformLocation(prog, 'u_rows'); - const uCols = gl.getUniformLocation(prog, 'u_cols'); - const uLineWidth = gl.getUniformLocation(prog, 'u_lineWidth'); - - // Single draw call state - gl.disable(gl.DEPTH_TEST); - gl.disable(gl.CULL_FACE); - gl.viewport(0, 0, canvas.width, canvas.height); - - // Resize handling (devicePixelRatio-aware) - const resize = () => { - const dpr = Math.max(window.devicePixelRatio || 1, 1); - const { clientWidth, clientHeight } = canvas; - const w = Math.max(1, Math.floor(clientWidth * dpr)); - const h = Math.max(1, Math.floor(clientHeight * dpr)); - if (canvas.width !== w || canvas.height !== h) { - canvas.width = w; - canvas.height = h; - gl.viewport(0, 0, w, h); - } - }; - - const observer = new ResizeObserver(resize); - observer.observe(canvas); - resize(); - - // Mouse tracking in device pixels - const onMove = (e: PointerEvent) => { - const rect = canvas.getBoundingClientRect(); - const dpr = Math.max(window.devicePixelRatio || 1, 1); - const x = (e.clientX - rect.left) * dpr; - const y = (rect.bottom - e.clientY) * dpr; // flip Y to GL coords - mouseRef.current = { x, y }; - }; - const onLeave = () => { - mouseRef.current = null; - }; - canvas.addEventListener('pointermove', onMove); - canvas.addEventListener('pointerleave', onLeave); - - const t0 = performance.now(); - const tick = () => { - const t = (performance.now() - t0) / 1000; - gl.useProgram(prog); - gl.uniform2f(uResolution, canvas.width, canvas.height); - const m = mouseRef.current; - if (m) gl.uniform2f(uMouse, m.x, m.y); - else gl.uniform2f(uMouse, -1, -1); - gl.uniform1f(uTime, t); - gl.uniform1f(uRows, rows); - gl.uniform1f(uCols, cols); - gl.uniform1f(uLineWidth, lineWidth); - - gl.drawArrays(gl.TRIANGLES, 0, 3); - rafRef.current = requestAnimationFrame(tick); - }; - tick(); - - return () => { - if (rafRef.current) cancelAnimationFrame(rafRef.current); - observer.disconnect(); - canvas.removeEventListener('pointermove', onMove); - canvas.removeEventListener('pointerleave', onLeave); - gl.deleteProgram(prog); - gl.deleteShader(vs); - gl.deleteShader(fs); - }; - }, [ rows, cols, lineWidth ]); - - return ( -
      - -
      - ); -} diff --git a/frontend/components/ConstructionPlaceHolder.tsx b/frontend/components/ConstructionPlaceHolder.tsx index 803d41a..40568a2 100644 --- a/frontend/components/ConstructionPlaceHolder.tsx +++ b/frontend/components/ConstructionPlaceHolder.tsx @@ -4,7 +4,7 @@ const ConstructionPlaceHolder = () => { return (

      - 正在施工中... + * Under Construction *

      ); diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index 5036a02..6418200 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -1,3 +1,5 @@ +'use client'; + import React from 'react'; import Link from 'next/link'; import { IoLogoGithub } from 'react-icons/io5'; diff --git a/frontend/src/api/blog/post.ts b/frontend/src/api/blog/post.ts index 5dedce7..f1046f8 100644 --- a/frontend/src/api/blog/post.ts +++ b/frontend/src/api/blog/post.ts @@ -26,9 +26,26 @@ export interface PostListResponse { status: string } +interface FetchPostListParams { + page: number; + page_size?: number; + tag_filter?: string[]; + match_all?: boolean; +} -export async function fetchPostList(page: number, page_size: number = DEFAULT_PAGE_SIZE): Promise { - const url = `/blog/posts?page=${ page }&page_size=${ page_size }`; +export async function fetchPostList({ + page, + page_size = DEFAULT_PAGE_SIZE, + tag_filter = [], + match_all = false + }: FetchPostListParams +): Promise { + const pagination_string = `page=${ page }&page_size=${ page_size }`; + const tag_filter_string = tag_filter.length != 0 + ? `&tags=${ tag_filter.join(',') }&match_all=${ match_all }` + : ''; + + const url = `/blog/posts?${ pagination_string }${ tag_filter_string }`; return await fetchNocturneJson(url); }