Skip to content

Commit 032d1ea

Browse files
committed
feat: knowledge graph visualization, site-wide banners, and brutalist UI refactor
- Implemented FEZ-16: 3D Knowledge Graph Visualization Protocol (/graph) - Implemented FEZ-22: Site-wide banner system with PIML-based scheduling and dismissal - Redesigned 404 page and loading states with brutalist system-error theme - Refactored About section to use nested path routing and native window scrolling - Fixed persistent scroll-lock bugs across About page and Modals - Updated all Phosphor icons to the non-deprecated 'Icon' suffix version - Optimized SEO default images for the apps section - Added new blogpost and timeline entry for the Knowledge Graph feature
1 parent 5fe3dd2 commit 032d1ea

File tree

8 files changed

+227
-54
lines changed

8 files changed

+227
-54
lines changed

public/banner.piml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
(banners)
2+
> (banner)
3+
(id) launch-notification
4+
(type) info
5+
(from) 2025-12-21T00:00:00Z
6+
(to) 2025-12-23T23:59:59Z
7+
(text) NEURAL NET ONLINE: THE 3D KNOWLEDGE GRAPH VISUALIZATION PROTOCOL HAS BEEN DEPLOYED. ACCESS AT /GRAPH.
8+
(isActive) true
9+
(link) /graph
10+
(linkText) See Neural Net

public/roadmap/roadmap.piml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,10 @@
269269
4. Text
270270
(category) Aesthetic
271271
(epic) OS Experience
272-
(status) Planned
272+
(status) Completed
273273
(priority) Medium
274274
(created_at) 2025-12-21T18:00:00+03:00
275-
(notes)
275+
(notes) Implemented via public/banner.piml and src/components/Banner.js. Supports types (info, warning, error), scheduling (from/to), links, and persistence (localStorage dismissal).
276276

277277

278278

src/components/AppCard.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from 'react';
22
import { Link } from 'react-router-dom';
3-
import { ArrowRight, Star } from '@phosphor-icons/react';
3+
import { ArrowRightIcon, StarIcon } from '@phosphor-icons/react';
44
import { motion } from 'framer-motion';
55
import GenerativeArt from './GenerativeArt';
66
import { appIcons } from '../utils/appIcons';
77

88
const AppCard = ({ app }) => {
99
const { to, title, description, icon, pinned_order } = app;
10-
const Icon = appIcons[icon] || appIcons[`${icon}Icon`] || Star;
10+
const Icon = appIcons[icon] || appIcons[`${icon}Icon`] || StarIcon;
1111

1212
return (
1313
<motion.div
@@ -33,7 +33,7 @@ const AppCard = ({ app }) => {
3333
{/* Pinned Badge */}
3434
{pinned_order && (
3535
<div className="absolute top-3 right-3 text-yellow-400 drop-shadow-md">
36-
<Star weight="fill" size={18} />
36+
<StarIcon weight="fill" size={18} />
3737
</div>
3838
)}
3939
</div>
@@ -52,7 +52,7 @@ const AppCard = ({ app }) => {
5252
<span className="text-[10px] font-mono font-bold uppercase tracking-widest text-gray-500 group-hover:text-white transition-colors">
5353
Open App
5454
</span>
55-
<ArrowRight
55+
<ArrowRightIcon
5656
weight="bold"
5757
size={14}
5858
className="text-emerald-500 transform -translate-x-2 opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100"

src/components/Banner.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React, { useState, useEffect } from 'react';
2+
import piml from 'piml';
3+
import { motion, AnimatePresence } from 'framer-motion';
4+
import { InfoIcon, WarningIcon, WarningOctagonIcon, XIcon, ArrowRightIcon } from '@phosphor-icons/react';
5+
import { Link } from 'react-router-dom';
6+
7+
const DISMISSED_BANNERS_KEY = 'dismissed-banners';
8+
9+
const Banner = () => {
10+
const [banner, setBanner] = useState(null);
11+
const [isVisible, setIsVisible] = useState(true);
12+
13+
useEffect(() => {
14+
const fetchBanner = async () => {
15+
try {
16+
const response = await fetch('/banner.piml');
17+
if (!response.ok) return;
18+
const text = await response.text();
19+
const parsed = piml.parse(text);
20+
21+
let bannerList = [];
22+
if (parsed.banners) {
23+
if (Array.isArray(parsed.banners)) {
24+
bannerList = parsed.banners;
25+
} else if (parsed.banners.banner) {
26+
bannerList = Array.isArray(parsed.banners.banner)
27+
? parsed.banners.banner
28+
: [parsed.banners.banner];
29+
} else {
30+
bannerList = [parsed.banners];
31+
}
32+
} else if (parsed.banner) {
33+
bannerList = Array.isArray(parsed.banner)
34+
? parsed.banner
35+
: [parsed.banner];
36+
}
37+
38+
const now = new Date();
39+
const dismissedBannersRaw = localStorage.getItem(DISMISSED_BANNERS_KEY);
40+
const dismissedBanners = dismissedBannersRaw
41+
? JSON.parse(dismissedBannersRaw)
42+
: [];
43+
44+
const activeBanner = bannerList.find((b) => {
45+
const isActive = String(b.isActive).toLowerCase() === 'true';
46+
if (!isActive) return false;
47+
48+
const fromDate = b.from ? new Date(b.from) : null;
49+
const toDate = b.to ? new Date(b.to) : null;
50+
51+
if (fromDate && now < fromDate) return false;
52+
if (toDate && now > toDate) return false;
53+
54+
// Check dismissal from array
55+
return !dismissedBanners.includes(b.id);
56+
});
57+
58+
if (activeBanner) {
59+
setBanner(activeBanner);
60+
}
61+
} catch (error) {
62+
console.error('Failed to fetch banner:', error);
63+
}
64+
};
65+
66+
fetchBanner();
67+
}, []);
68+
69+
const handleDismiss = () => {
70+
setIsVisible(false);
71+
if (banner && banner.id) {
72+
const dismissedBannersRaw = localStorage.getItem(DISMISSED_BANNERS_KEY);
73+
const dismissedBanners = dismissedBannersRaw
74+
? JSON.parse(dismissedBannersRaw)
75+
: [];
76+
77+
if (!dismissedBanners.includes(banner.id)) {
78+
dismissedBanners.push(banner.id);
79+
localStorage.setItem(
80+
DISMISSED_BANNERS_KEY,
81+
JSON.stringify(dismissedBanners),
82+
);
83+
}
84+
}
85+
};
86+
87+
if (!banner || !isVisible) return null;
88+
89+
const getTypeStyles = () => {
90+
switch (banner.type) {
91+
case 'error':
92+
return {
93+
bg: 'bg-red-600',
94+
text: 'text-white',
95+
icon: <WarningOctagonIcon size={20} weight="bold" />,
96+
accent: 'border-red-400'
97+
};
98+
case 'warning':
99+
return {
100+
bg: 'bg-amber-500',
101+
text: 'text-black',
102+
icon: <WarningIcon size={20} weight="bold" />,
103+
accent: 'border-amber-300'
104+
};
105+
case 'info':
106+
default:
107+
return {
108+
bg: 'bg-emerald-600',
109+
text: 'text-white',
110+
icon: <InfoIcon size={20} weight="bold" />,
111+
accent: 'border-emerald-400'
112+
};
113+
}
114+
};
115+
116+
const styles = getTypeStyles();
117+
118+
return (
119+
<AnimatePresence>
120+
{isVisible && (
121+
<motion.div
122+
initial={{ height: 0, opacity: 0 }}
123+
animate={{ height: 'auto', opacity: 1 }}
124+
exit={{ height: 0, opacity: 0 }}
125+
className={`${styles.bg} ${styles.text} relative z-[100] border-b-2 border-black selection:bg-white selection:text-black`}
126+
>
127+
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between gap-4">
128+
<div className="flex items-center gap-3 flex-1">
129+
<span className="shrink-0">{styles.icon}</span>
130+
<p className="font-mono text-xs md:text-sm font-black uppercase tracking-widest leading-tight">
131+
{banner.text}
132+
</p>
133+
{banner.link && (
134+
<Link
135+
to={banner.link}
136+
className="shrink-0 inline-flex items-center gap-1 bg-black/20 hover:bg-black/40 px-3 py-1 rounded-sm border border-white/20 transition-all font-bold text-[10px] uppercase"
137+
>
138+
{banner.linkText || 'VIEW_DETAILS'}
139+
<ArrowRightIcon size={12} weight="bold" />
140+
</Link>
141+
)}
142+
</div>
143+
144+
<button
145+
onClick={handleDismiss}
146+
className="p-1 hover:bg-black/20 rounded-sm transition-colors shrink-0"
147+
aria-label="Dismiss"
148+
>
149+
<XIcon size={20} weight="bold" />
150+
</button>
151+
</div>
152+
153+
{/* Brutalist glitch line at bottom */}
154+
<div className="h-0.5 w-full bg-black/10" />
155+
</motion.div>
156+
)}
157+
</AnimatePresence>
158+
);
159+
};
160+
161+
export default Banner;

0 commit comments

Comments
 (0)