Skip to content

Commit 50bbc6d

Browse files
committed
new post.
1 parent b44ad47 commit 50bbc6d

File tree

7 files changed

+251
-10
lines changed

7 files changed

+251
-10
lines changed

public/posts/implementing-a-sliding-side-panel.txt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# Implementing a Resizable Global Side Panel in React
2-
31
Sometimes, a modal is just too intrusive. You want to show detailed context—like a complex rating system or metadata—without forcing the user to lose their place on the page or blocking the entire UI with a backdrop that demands immediate attention. Enter the **Sliding Side Panel**.
42

53
In this post, I'll walk through how I implemented a global side panel system for **Fezcodex**, allowing any component in the app to trigger a content-rich overlay that slides in smoothly from the right. Even better? I made it **resizable**, so users can drag to expand the view if they need more space.
@@ -14,7 +12,7 @@ To make this truly reusable, I avoided prop drilling by using the **Context API*
1412

1513
### Why Context? Avoiding Prop Drilling
1614

17-
Without a global context, implementing a feature like this would require **prop drilling**. This is a common pattern (or anti-pattern) in React where you pass data or functions down through multiple layers of components just to get them to where they are needed.
15+
Without a global context, implementing a feature like this would require **[prop drilling](/#/vocab/prop-drilling)**. This is a common pattern (or **[anti-pattern](/#/vocab/anti-pattern)**) in React where you pass data or functions down through multiple layers of components just to get them to where they are needed.
1816

1917
Imagine we managed the side panel state in `App.js`. We would have to pass the `openSidePanel` function like this:
2018

@@ -117,7 +115,7 @@ const SidePanel = () => {
117115
{isOpen && (
118116
<>
119117
<motion.div onClick={closeSidePanel} className="fixed inset-0 bg-black/50 z-[60]" />
120-
118+
121119
<motion.div
122120
initial={{ x: '100%' }}
123121
animate={{ x: 0 }}
@@ -131,7 +129,7 @@ const SidePanel = () => {
131129
onMouseDown={(e) => { setIsResizing(true); e.preventDefault(); }}
132130
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-ew-resize hover:bg-primary-500/50 transition-colors z-50"
133131
/>
134-
132+
135133
{/* Header & Content */}
136134
</motion.div>
137135
</>
@@ -153,4 +151,4 @@ The side panel proved perfect for this: users can check the rating criteria with
153151

154152
## Conclusion
155153

156-
Global UI elements controlled via Context are a powerful pattern in React. By adding a simple resize handle and managing width in the global state, we've transformed a static overlay into a flexible, user-friendly tool that adapts to the user's needs.
154+
Global UI elements controlled via Context are a powerful pattern in React. By adding a simple resize handle and managing width in the global state, we've transformed a static overlay into a flexible, user-friendly tool that adapts to the user's needs.

public/posts/posts.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
[
2+
{
3+
"slug": "react-magic-markdown-components",
4+
"title": "React Magic: Rendering Components from Markdown Links",
5+
"date": "2025-12-12",
6+
"updated": "2025-12-12",
7+
"description": "How to trigger complex React interactions and render dynamic components directly from static Markdown links.",
8+
"tags": ["react", "markdown", "ui/ux", "frontend", "patterns"],
9+
"category": "dev",
10+
"filename": "react-magic-markdown-components.txt",
11+
"authors": ["fezcode"],
12+
"image": "/images/defaults/visuals-2TS23o0-pUc-unsplash.jpg"
13+
},
214
{
315
"slug": "implementing-a-sliding-side-panel",
4-
"title": "Implementing a Global Sliding Side Panel in React",
16+
"title": "Implementing a Resizable Global Sliding Side Panel in React",
517
"date": "2025-12-11",
618
"updated": "2025-12-11",
719
"description": "How I built a context-driven, global sliding side panel system for Fezcodex using React and Framer Motion.",
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
Static text is boring. In a modern React application, your content should be alive.
2+
3+
Today I want to share a fun pattern I implemented in **Fezcodex**: triggering dynamic UI interactions directly from standard Markdown links. Specifically, clicking a link in a blog post to open a side panel with a live React component, rather than navigating to a new page.
4+
5+
## The Idea
6+
7+
I wanted to explain technical terms like **[Prop Drilling](/#/vocab/prop-drilling)** without forcing the reader to leave the article. A tooltip is too small; a new tab is too distracting. The solution? My global **Side Panel**.
8+
9+
But how do you tell a static Markdown file to "render a React component in the side panel"?
10+
11+
## The Solution
12+
13+
The secret sauce lies in `react-markdown`'s ability to customize how HTML elements are rendered. We can intercept every `<a>` tag and check if it's a "special" link.
14+
15+
### 1. The Interceptor (`MarkdownLink`)
16+
17+
I created a custom component that replaces standard HTML anchors. It checks the `href` for a specific pattern (in my case, `/vocab/`).
18+
19+
```javascript
20+
const MarkdownLink = ({ href, children }) => {
21+
const { openSidePanel } = useSidePanel();
22+
23+
// Check if this is a "vocabulary" link
24+
const isVocab = href && href.includes('/vocab/');
25+
26+
if (isVocab) {
27+
// 1. Extract the term ID (e.g., "prop-drilling")
28+
const term = href.split('/vocab/')[1];
29+
30+
// 2. Look up the definition/component
31+
const definition = vocabulary[term];
32+
33+
return (
34+
<a
35+
href={href}
36+
onClick={(e) => {
37+
e.preventDefault(); // Stop navigation!
38+
if (definition) {
39+
// 3. Trigger the global UI
40+
openSidePanel(definition.title, definition.content);
41+
}
42+
}}
43+
className="text-amber-400 dashed-underline cursor-help"
44+
>
45+
{children}
46+
</a>
47+
);
48+
}
49+
50+
// Fallback for normal links
51+
return <a href={href}>{children}</a>;
52+
};
53+
```
54+
55+
### 2. The Data (`vocabulary.js`)
56+
57+
I store the actual content in a simple lookup object. The beauty is that `content` can be *anything*--text, images, or fully interactive React components.
58+
59+
```javascript
60+
export const vocabulary = {
61+
'prop-drilling': {
62+
title: 'Prop Drilling',
63+
content: <PropDrillingDiagram /> // A real component!
64+
},
65+
// ...
66+
};
67+
```
68+
69+
### 3. Handling "Deep Links"
70+
71+
What if someone actually copies the URL `https://fezcodex.com/#/vocab/prop-drilling` and sends it to a friend? The `onClick` handler won't fire because they aren't clicking a link—they are loading the app.
72+
73+
To handle this, I added a "phantom" route in my Router:
74+
75+
```javascript
76+
// VocabRouteHandler.js
77+
const VocabRouteHandler = () => {
78+
const { term } = useParams();
79+
const navigate = useNavigate();
80+
const { openSidePanel } = useSidePanel();
81+
82+
useEffect(() => {
83+
// 1. Open the panel immediately
84+
if (vocabulary[term]) {
85+
openSidePanel(vocabulary[term].title, vocabulary[term].content);
86+
}
87+
// 2. Redirect to home (so the background isn't blank)
88+
navigate('/', { replace: true });
89+
}, [term]);
90+
91+
return null;
92+
};
93+
```
94+
95+
## Why this rocks
96+
97+
This pattern effectively turns your static Markdown content into a control surface for your application. You can write:
98+
99+
> "Check out this `[interactive demo](/#/demos/sorting-algo)`..."
100+
101+
And have it launch a full-screen visualization, a game, or a configuration wizard, all without leaving the flow of your writing. It bridges the gap between "content" and "app".

src/components/AnimatedRoutes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const NewsPage = lazy(() => import('../pages/NewsPage'));
149149
const CommandsPage = lazy(() => import('../pages/CommandsPage'));
150150
const AchievementsPage = lazy(() => import('../pages/AchievementsPage'));
151151
const SitemapPage = lazy(() => import('../pages/SitemapPage'));
152+
const VocabRouteHandler = lazy(() => import('../components/VocabRouteHandler'));
152153

153154
const pageVariants = {
154155
initial: {
@@ -398,6 +399,14 @@ function AnimatedRoutes() {
398399
</motion.div>
399400
}
400401
/>
402+
<Route
403+
path="/vocab/:term"
404+
element={
405+
<Suspense fallback={<Loading />}>
406+
<VocabRouteHandler />
407+
</Suspense>
408+
}
409+
/>
401410
<Route
402411
path="*"
403412
element={
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useEffect } from 'react';
2+
import { useParams, useNavigate } from 'react-router-dom';
3+
import { useSidePanel } from '../context/SidePanelContext';
4+
import { vocabulary } from '../data/vocabulary';
5+
6+
const VocabRouteHandler = () => {
7+
const { term } = useParams();
8+
const navigate = useNavigate();
9+
const { openSidePanel } = useSidePanel();
10+
11+
useEffect(() => {
12+
if (term && vocabulary[term]) {
13+
const def = vocabulary[term];
14+
openSidePanel(def.title, def.content);
15+
}
16+
// Redirect to home so the background isn't empty/404.
17+
// In a real app, we might want to stay on the "previous" page, but we don't know what that is on hard refresh.
18+
navigate('/', { replace: true });
19+
}, [term, navigate, openSidePanel]);
20+
21+
return null;
22+
};
23+
24+
export default VocabRouteHandler;

src/data/vocabulary.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
3+
export const vocabulary = {
4+
'prop-drilling': {
5+
title: 'Prop Drilling',
6+
content: (
7+
<div className="space-y-4">
8+
<p>
9+
<strong>Prop Drilling</strong> (also known as "threading") refers to the process of passing data from a parent component down to a deeply nested child component through intermediate components that do not need the data themselves.
10+
</p>
11+
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700 font-mono text-xs">
12+
<div className="text-blue-400">{'<App data={data} />'}</div>
13+
<div className="pl-4 text-gray-500">{'↓'}</div>
14+
<div className="pl-4 text-purple-400">{'<Layout data={data} />'}</div>
15+
<div className="pl-8 text-gray-500">{'↓'}</div>
16+
<div className="pl-8 text-green-400">{'<Page data={data} />'}</div>
17+
<div className="pl-12 text-gray-500">{'↓'}</div>
18+
<div className="pl-12 text-red-400">{'<DeepComponent data={data} />'}</div>
19+
</div>
20+
<p>
21+
While simple, it becomes problematic as the application grows, leading to:
22+
</p>
23+
<ul className="list-disc pl-5 space-y-1 text-gray-400">
24+
<li>Code verbosity</li>
25+
<li>Tight coupling between components</li>
26+
<li>Difficulty in refactoring</li>
27+
<li>Unnecessary re-renders</li>
28+
</ul>
29+
<p>
30+
<strong>Solution:</strong> Use the <em>Context API</em>, <em>Redux</em>, or similar state management libraries to make data accessible to any component in the tree without manual passing.
31+
</p>
32+
</div>
33+
),
34+
},
35+
'context-api': {
36+
title: 'Context API',
37+
content: (
38+
<div className="space-y-4">
39+
<p>
40+
The <strong>Context API</strong> is a React feature that enables you to share values (like global settings, user auth, or themes) between components without having to explicitly pass a prop through every level of the tree.
41+
</p>
42+
</div>
43+
)
44+
},
45+
'anti-pattern': {
46+
title: 'Anti-Pattern',
47+
content: (
48+
<div className="space-y-4">
49+
<p>
50+
An <strong>Anti-Pattern</strong> is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive.
51+
</p>
52+
<p>
53+
It's a solution that looks good on the surface but has bad consequences in the long run.
54+
</p>
55+
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
56+
<h4 className="text-sm font-bold text-red-400 mb-2">Common React Anti-Patterns:</h4>
57+
<ul className="list-disc pl-5 space-y-1 text-sm text-gray-400">
58+
<li>Prop Drilling (passing props down 5+ levels)</li>
59+
<li>Defining components inside other components</li>
60+
<li>Using indexes as keys in lists (when items can change order)</li>
61+
<li>Mutating state directly</li>
62+
</ul>
63+
</div>
64+
</div>
65+
),
66+
},
67+
};

src/pages/BlogPostPage.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,41 @@ import remarkGfm from 'remark-gfm';
2020
import rehypeRaw from 'rehype-raw';
2121
import { calculateReadingTime } from '../utils/readingTime';
2222
import { useAchievements } from '../context/AchievementContext';
23+
import { useSidePanel } from '../context/SidePanelContext';
24+
import { vocabulary } from '../data/vocabulary';
2325

2426
// --- Helper Components ---
2527

26-
const LinkRenderer = ({ href, children }) => {
27-
const isExternal = href.startsWith('http') || href.startsWith('https');
28+
const MarkdownLink = ({ href, children }) => {
29+
const { openSidePanel } = useSidePanel();
30+
const isExternal = href?.startsWith('http') || href?.startsWith('https');
31+
const isVocab = href && (href.startsWith('/vocab/') || href.includes('/#/vocab/'));
32+
33+
if (isVocab) {
34+
// Extract term: handle both /vocab/term and /#/vocab/term
35+
const parts = href.split('/vocab/');
36+
const term = parts[1];
37+
const definition = vocabulary[term];
38+
39+
return (
40+
<a
41+
href={href}
42+
onClick={(e) => {
43+
e.preventDefault();
44+
if (definition) {
45+
openSidePanel(definition.title, definition.content);
46+
} else {
47+
console.warn(`Vocabulary term not found: ${term}`);
48+
}
49+
}}
50+
className="text-amber-400 hover:text-amber-300 transition-colors inline-flex items-center gap-1 border-b border-amber-500/30 border-dashed hover:border-solid cursor-help"
51+
title="Click for definition"
52+
>
53+
{children}
54+
</a>
55+
);
56+
}
57+
2858
return (
2959
<a
3060
href={href}
@@ -370,7 +400,7 @@ const BlogPostPage = () => {
370400
remarkPlugins={[remarkGfm]}
371401
rehypePlugins={[rehypeRaw]}
372402
components={{
373-
a: LinkRenderer,
403+
a: MarkdownLink,
374404
pre: ({ children }) => <>{children}</>,
375405
code: CodeBlock,
376406
img: ImageRenderer,

0 commit comments

Comments
 (0)