articles canvas mode

This commit is contained in:
2026-04-13 01:26:21 -05:00
parent 8c47404d02
commit 0aa0cc72a7
7 changed files with 65 additions and 69 deletions

View File

@@ -1,44 +0,0 @@
import {createContext, PropsWithChildren, useContext, useEffect, useState} from "react";
import {Reader} from "./Reader";
import {Article, ARTICLES} from "./articles";
import {useRouter} from "../router/RouterContext";
import {Path} from "../router/router";
const articleContext =
createContext<ReturnType<typeof useState<Article | undefined>>>([undefined, function(){}]);
export function useArticle() {
return useContext(articleContext);
}
const ARTICLE_QUERY = "a";
export default function ArticleProvider({ children }: PropsWithChildren) {
const [article, setArticle] = useState<Article | undefined>(undefined);
const router = useRouter();
useEffect(() => {
const params = new URLSearchParams(location.search);
if(params.has(ARTICLE_QUERY)) {
const id = Number.parseInt(params.get(ARTICLE_QUERY)!);
setArticle(ARTICLES[id]);
}
}, [setArticle]);
useEffect(() => {
if(!article) {
// query param
const params = new URLSearchParams(location.search);
if(params.has(ARTICLE_QUERY)) {
router.replacePage(location.pathname as Path);
}
} else {
const id = ARTICLES.indexOf(article);
router.replacePage(`${location.pathname}?${ARTICLE_QUERY}=${id}` as Path);
}
}, [article]);
return <articleContext.Provider value={[article, setArticle]}>
<Reader/>
{children}
</articleContext.Provider>;
}

View File

@@ -1,28 +1,67 @@
import {useArticle} from "./ArticleContext";
import "./list.sass"; import "./list.sass";
import {ReactComponent as BoxSvg} from "../img/blog-box.svg"; import {ReactComponent as BoxSvg} from "../img/blog-box.svg";
import {Article, ARTICLES} from "./articles"; import {Article, ARTICLES} from "./articles";
import {motion} from "motion/react"; import {motion} from "motion/react";
import {AnimateFade} from "../animations"; import {AnimateFade} from "../animations";
import {Canvas} from "@react-three/fiber";
import {Environment, Html, OrbitControls} from "@react-three/drei";
import MilkyWay from "../img/2k_stars_milky_way.jpg";
import {Sun} from "../index/Canvas";
import {Bloom, EffectComposer, Vignette} from "@react-three/postprocessing";
import {useEffect, useState} from "react";
import {Reader} from "./Reader";
import {useRouter} from "../router/RouterContext";
import {Path} from "../router/router";
const ARTICLE_QUERY = "a";
const main = AnimateFade(); const main = AnimateFade();
export default function ArticlesList() { export default function ArticlesList() {
return <motion.main animate={main.animate} initial={main.initial} className="art-ctr"> const [article, setArticle] = useState<Article | null>(null);
<section className="spacer"/> const router = useRouter();
<section className="articles">
{ useEffect(() => {
ARTICLES.map(a => const params = new URLSearchParams(location.search);
<BlogBox key={a.name} article={a} /> if(params.has(ARTICLE_QUERY)) {
) const id = Number.parseInt(params.get(ARTICLE_QUERY)!);
setArticle(ARTICLES[id] ?? null);
}
}, [setArticle]);
useEffect(() => {
if(!article) {
// query param
const params = new URLSearchParams(location.search);
if(params.has(ARTICLE_QUERY)) {
router.replacePage(location.pathname as Path);
} }
</section> } else {
<section className="spacer"/> const id = ARTICLES.indexOf(article);
router.replacePage(`${location.pathname}?${ARTICLE_QUERY}=${id}` as Path);
}
}, [article]);
return <motion.main animate={main.animate} initial={main.initial} className="art-ctr">
<Canvas camera={{position: [0,0,20]}}>
<Environment background files={MilkyWay}/>
<EffectComposer>
<Bloom luminanceThreshold={0} luminanceSmoothing={0.9} />
<Vignette eskil={false} offset={0.1} darkness={1.1} />
</EffectComposer>
<Sun/>
<OrbitControls enablePan={false} enableZoom />
<Html className="articles" zIndexRange={[100,0]} prepend center transform>
{
ARTICLES.map(a => <BlogBox setReading={setArticle} article={a}/>)
}
</Html>
</Canvas>
<Reader post={article} setPost={setArticle}/>
</motion.main> </motion.main>
} }
function BlogBox(props: { article: Article }) { function BlogBox(props: { article: Article, setReading: (_: Article | null) => void }) {
const {article, article: {name, description, thumbnail, published} } = props; const {article, setReading, article: {name, description, thumbnail, published} } = props;
const [, setReading] = useArticle();
return ( return (
<button className="link" onClick={() => setReading(article)}> <button className="link" onClick={() => setReading(article)}>

View File

@@ -1,4 +1,3 @@
import {useArticle} from "./ArticleContext";
import {animate, motion} from "framer-motion"; import {animate, motion} from "framer-motion";
import {useEffect} from "react"; import {useEffect} from "react";
import {AnimateFade, AnimateTemplate} from "../animations"; import {AnimateFade, AnimateTemplate} from "../animations";
@@ -6,11 +5,11 @@ import "./reader.sass";
import {ExternalLink} from "../router/Link"; import {ExternalLink} from "../router/Link";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faClose} from "@fortawesome/free-solid-svg-icons/faClose"; import {faClose} from "@fortawesome/free-solid-svg-icons/faClose";
import {Article} from "./articles";
const READER : AnimateTemplate = AnimateFade(); const READER : AnimateTemplate = AnimateFade();
export function Reader() { export function Reader({post, setPost}: {post: Article | null, setPost: (_: Article | null) => void}) {
const [post, setPost] = useArticle();
useEffect(() => { useEffect(() => {
function pressEscape(e: KeyboardEvent) { function pressEscape(e: KeyboardEvent) {
@@ -23,7 +22,7 @@ export function Reader() {
function handleClose() { function handleClose() {
animate(".reader", READER.initial) animate(".reader", READER.initial)
.then(() => setPost(undefined)); .then(() => setPost(null));
} }
return post ? ( return post ? (

View File

@@ -1,6 +1,11 @@
@use "../nav/nav" @use "../nav/nav"
@use "../index" @use "../index"
$entry-width: 400px $entry-width: 400px
canvas
width: 100vw
height: calc(100vh - 100px)
.blog-box .blog-box
display: flex display: flex
flex-direction: column flex-direction: column
@@ -39,6 +44,7 @@ $entry-width: 400px
color: grey color: grey
.articles .articles
z-index: -999
display: flex display: flex
flex-grow: 3 flex-grow: 3
justify-content: center justify-content: center

View File

@@ -23,7 +23,6 @@
align-items: center align-items: center
border-radius: 15px border-radius: 15px
margin: auto margin: auto
padding: 30px
overflow-y: scroll overflow-y: scroll
overflow-x: hidden overflow-x: hidden
background: white background: white
@@ -64,4 +63,4 @@
font-weight: bold font-weight: bold
color: red !important color: red !important
background: white background: white
border-radius: 15px border-radius: 999px

View File

@@ -5,7 +5,6 @@ import {RouterProvider} from "./router/RouterContext";
import "@fontsource/inter"; // Defaults to weight 400 import "@fontsource/inter"; // Defaults to weight 400
import "@fontsource/neuton"; import "@fontsource/neuton";
import {Routes} from "./router/Route"; import {Routes} from "./router/Route";
import ArticleProvider from "./articles/ArticleContext";
const Nav = React.lazy(() => import("./nav/Nav")); const Nav = React.lazy(() => import("./nav/Nav"));
@@ -16,10 +15,8 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<RouterProvider> <RouterProvider>
<ArticleProvider> <Nav/>
<Nav/> <Routes/>
<Routes/>
</ArticleProvider>
</RouterProvider> </RouterProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -45,7 +45,7 @@ export default function Canvas() {
) )
} }
function Sun() { export function Sun() {
return ( return (
<mesh position={[-100, 0, -100]}> <mesh position={[-100, 0, -100]}>
<sphereGeometry args={[5, 32, 32]} /> <sphereGeometry args={[5, 32, 32]} />