Skip to content

Commit 45b76ed

Browse files
committed
content(app): fezGlyph
1 parent e4c75da commit 45b76ed

File tree

3 files changed

+335
-0
lines changed

3 files changed

+335
-0
lines changed

public/apps/apps.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,14 @@
637637
"description": "Convert between px, em, rem, vw, vh, and % units.",
638638
"icon": "RulerIcon",
639639
"created_at": "2025-11-07T19:47:38+03:00"
640+
},
641+
{
642+
"slug": "fezglyph",
643+
"to": "/apps/fezglyph",
644+
"title": "FezGlyph",
645+
"description": "Transform text into the cryptic Fezcodex symbolic cipher.",
646+
"icon": "PenNibIcon",
647+
"created_at": "2025-12-28T12:00:00+03:00"
640648
}
641649
]
642650
},

src/components/AnimatedRoutes.jsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ const MagazinerPage = lazy(() => import('../pages/apps/MagazinerPage'));
156156
const WallpaperEnginePage = lazy(() => import('../pages/apps/WallpaperEnginePage'));
157157
const SymbolFlowPage = lazy(() => import('../pages/apps/SymbolFlowPage'));
158158
const JsMasterclassPage = lazy(() => import('../pages/apps/JsMasterclassPage'));
159+
const FezGlyphPage = lazy(() => import('../pages/apps/FezGlyphPage'));
159160
const AssetConstructorPage = lazy(() => import('../pages/apps/AssetConstructorPage'));
160161
const CodeblockCreatorPage = lazy(() => import('../pages/apps/CodeblockCreatorPage'));
161162
const AlbumCoverPage = lazy(() => import('../pages/apps/AlbumCoverPage'));
@@ -1074,6 +1075,10 @@ const AnimatedRoutes = ({
10741075
path="/apps::we"
10751076
element={<Navigate to="/apps/wallpaper-engine" replace />}
10761077
/>
1078+
<Route
1079+
path="/apps::fg"
1080+
element={<Navigate to="/apps/fezglyph" replace />}
1081+
/>
10771082
{/* End of hardcoded redirects */}
10781083
<Route
10791084
path="/apps::code"
@@ -2248,6 +2253,22 @@ const AnimatedRoutes = ({
22482253
</motion.div>
22492254
}
22502255
/>
2256+
<Route
2257+
path="/apps/fezglyph"
2258+
element={
2259+
<motion.div
2260+
initial="initial"
2261+
animate="in"
2262+
exit="out"
2263+
variants={pageVariants}
2264+
transition={pageTransition}
2265+
>
2266+
<Suspense fallback={<Loading />}>
2267+
<FezGlyphPage />
2268+
</Suspense>
2269+
</motion.div>
2270+
}
2271+
/>
22512272
<Route
22522273
path="/apps/lorem-ipsum-generator"
22532274
element={

src/pages/apps/FezGlyphPage.jsx

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import React, { useState } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
TranslateIcon,
6+
PenNibIcon,
7+
NotebookIcon,
8+
CopyIcon,
9+
CheckIcon,
10+
} from '@phosphor-icons/react';
11+
import useSeo from '../../hooks/useSeo';
12+
import { useToast } from '../../hooks/useToast';
13+
import GenerativeArt from '../../components/GenerativeArt';
14+
import BreadcrumbTitle from '../../components/BreadcrumbTitle';
15+
16+
const SHORTHAND_MAP = {
17+
a: 'ᐱ',
18+
b: 'ᗷ',
19+
c: 'ᑕ',
20+
d: 'ᗪ',
21+
e: 'ᐦ',
22+
f: 'ᖴ',
23+
g: 'ᘏ',
24+
h: 'ᕼ',
25+
i: 'ᐃ',
26+
j: 'ᒧ',
27+
k: 'ᔘ',
28+
l: 'ᐳ',
29+
m: 'ᒄ',
30+
n: 'ᓀ',
31+
o: 'ᐤ',
32+
p: 'ᐯ',
33+
q: 'ᕟ',
34+
r: 'ᕃ',
35+
s: 'ᔆ',
36+
t: 'ᑎ',
37+
u: 'ᑌ',
38+
v: 'ᐪ',
39+
w: 'ᐎ',
40+
x: 'ᕁ',
41+
y: 'ᔅ',
42+
z: 'ᙆ',
43+
A: 'ᐱ',
44+
B: 'ᗷ',
45+
C: 'ᑕ',
46+
D: 'ᗪ',
47+
E: 'ᐦ',
48+
F: 'ᖴ',
49+
G: 'ᘏ',
50+
H: 'ᕼ',
51+
I: 'ᐃ',
52+
J: 'ᒧ',
53+
K: 'ᔘ',
54+
L: 'ᐳ',
55+
M: 'ᒄ',
56+
N: 'ᓀ',
57+
O: 'ᐤ',
58+
P: 'ᐯ',
59+
Q: 'ᕟ',
60+
R: 'ᕃ',
61+
S: 'ᔆ',
62+
T: 'ᑎ',
63+
U: 'ᑌ',
64+
V: 'ᐪ',
65+
W: 'ᐎ',
66+
X: 'ᕁ',
67+
Y: 'ᔅ',
68+
Z: 'ᙆ',
69+
1: 'ᐘ',
70+
2: 'ᐙ',
71+
3: 'ᐚ',
72+
4: 'ᐛ',
73+
5: 'ᐜ',
74+
6: 'ᐝ',
75+
7: 'ᐞ',
76+
8: 'ᐟ',
77+
9: 'ᐠ',
78+
0: 'ᐡ',
79+
' ': ' ',
80+
'.': '᙮',
81+
',': 'ᙵ',
82+
'?': 'ᙶ',
83+
'!': 'ᙷ',
84+
};
85+
86+
const REVERSE_SHORTHAND_MAP = Object.fromEntries(
87+
Object.entries(SHORTHAND_MAP).map(([key, value]) => [
88+
value,
89+
key.toLowerCase(),
90+
]),
91+
);
92+
93+
const FezGlyphPage = () => {
94+
const appName = 'FezGlyph';
95+
96+
useSeo({
97+
title: `${appName} | Fezcodex`,
98+
description: 'Transform text into the cryptic Fezcodex symbolic cipher.',
99+
keywords: [
100+
'Fezcodex',
101+
'fezglyph',
102+
'symbols',
103+
'cipher',
104+
'converter',
105+
'runes',
106+
],
107+
});
108+
109+
const { addToast } = useToast();
110+
const [text, setText] = useState('');
111+
const [shorthand, setShorthand] = useState('');
112+
const [isCopied, setIsCopied] = useState(false);
113+
114+
const handleTextChange = (e) => {
115+
const newText = e.target.value;
116+
setText(newText);
117+
const newShorthand = newText
118+
.split('')
119+
.map((char) => SHORTHAND_MAP[char] || char)
120+
.join('');
121+
setShorthand(newShorthand);
122+
};
123+
124+
const handleShorthandChange = (e) => {
125+
const newShorthand = e.target.value;
126+
setShorthand(newShorthand);
127+
const newText = newShorthand
128+
.split('')
129+
.map((char) => REVERSE_SHORTHAND_MAP[char] || char)
130+
.join('');
131+
setText(newText);
132+
};
133+
134+
const handleSymbolClick = (char, symbol) => {
135+
const newShorthand = shorthand + symbol;
136+
setShorthand(newShorthand);
137+
// Find the reverse mapping for the symbol.
138+
// Note: char passed here is the key from SHORTHAND_MAP (e.g. 'a'), which is what we want.
139+
// However, we need to respect the reverse logic: symbol -> char.
140+
// Since we know the pair (char, symbol), we can just append char.
141+
// But let's stick to the reverse map to be safe and consistent with typing.
142+
const mappedChar = REVERSE_SHORTHAND_MAP[symbol] || char;
143+
setText(text + mappedChar);
144+
};
145+
146+
const handleCopy = async () => {
147+
if (!shorthand) return;
148+
try {
149+
await navigator.clipboard.writeText(shorthand);
150+
setIsCopied(true);
151+
addToast({
152+
title: 'Success',
153+
message: 'FezGlyph copied to clipboard',
154+
type: 'success',
155+
});
156+
setTimeout(() => setIsCopied(false), 2000);
157+
} catch (err) {
158+
addToast({
159+
title: 'Error',
160+
message: 'Failed to copy',
161+
type: 'error',
162+
});
163+
}
164+
};
165+
166+
return (
167+
<div className="min-h-screen bg-[#050505] text-white selection:bg-emerald-500/30 font-sans">
168+
<div className="mx-auto max-w-7xl px-6 py-24 md:px-12">
169+
<header className="mb-20">
170+
<Link
171+
to="/apps"
172+
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-white transition-colors uppercase tracking-widest"
173+
>
174+
<ArrowLeftIcon weight="bold" />
175+
<span>Applications</span>
176+
</Link>
177+
178+
<div className="flex flex-col md:flex-row md:items-end justify-between gap-12">
179+
<div className="space-y-4">
180+
<BreadcrumbTitle
181+
title="FezGlyph"
182+
slug="fezglyph"
183+
variant="brutalist"
184+
/>
185+
<p className="text-xl text-gray-400 max-w-2xl font-light leading-relaxed">
186+
Proprietary symbolic cipher. Encrypt plaintext into the
187+
geometric Fezcodex rune system.
188+
</p>
189+
</div>
190+
</div>
191+
</header>
192+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-12">
193+
<div className="relative border border-white/10 bg-white/[0.02] p-8 rounded-sm group overflow-hidden">
194+
<div className="absolute inset-0 opacity-5 pointer-events-none">
195+
<GenerativeArt seed="TEXT_INPUT" className="w-full h-full" />
196+
</div>
197+
<h3 className="font-mono text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-6 flex items-center gap-2">
198+
<NotebookIcon weight="fill" className="text-emerald-500" />
199+
Plaintext_Input
200+
</h3>
201+
<textarea
202+
value={text}
203+
onChange={handleTextChange}
204+
className="w-full h-64 bg-black/40 border border-white/5 p-6 font-mono text-xl text-gray-300 focus:border-emerald-500 focus:outline-none transition-all rounded-sm resize-none scrollbar-hide"
205+
placeholder="Type your message here..."
206+
/>
207+
</div>
208+
209+
<div className="relative border border-white/10 bg-white/[0.02] p-8 rounded-sm group overflow-hidden">
210+
<div className="absolute inset-0 opacity-5 pointer-events-none">
211+
<GenerativeArt seed="SYMBOL_OUTPUT" className="w-full h-full" />
212+
</div>
213+
<div className="flex items-center justify-between mb-6">
214+
<h3 className="font-mono text-[10px] font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
215+
<PenNibIcon weight="fill" className="text-emerald-500" />
216+
FezGlyph_Output
217+
</h3>
218+
<button
219+
onClick={handleCopy}
220+
disabled={!shorthand}
221+
className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-emerald-500 hover:text-emerald-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
222+
>
223+
{isCopied ? (
224+
<>
225+
<CheckIcon weight="bold" />
226+
<span>Copied</span>
227+
</>
228+
) : (
229+
<>
230+
<CopyIcon weight="bold" />
231+
<span>Copy</span>
232+
</>
233+
)}
234+
</button>
235+
</div>
236+
<textarea
237+
value={shorthand}
238+
onChange={handleShorthandChange}
239+
className="w-full h-64 bg-black/40 border border-white/5 p-6 font-mono text-2xl text-emerald-400 focus:border-emerald-500 focus:outline-none transition-all rounded-sm resize-none scrollbar-hide"
240+
placeholder="ᐱᐦᗆᐤᗪ..."
241+
/>
242+
</div>
243+
</div>
244+
{/* Legend */}
245+
<div className="border border-white/10 bg-white/[0.02] p-8 rounded-sm">
246+
<h3 className="font-mono text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-6 flex items-center gap-2">
247+
<TranslateIcon weight="fill" className="text-emerald-500" />
248+
Symbol_Legend
249+
</h3>
250+
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-4">
251+
{Object.entries(SHORTHAND_MAP)
252+
.filter(([k]) => k.length === 1 && /[a-z0-9]/.test(k))
253+
.map(([char, symbol]) => (
254+
<button
255+
key={char}
256+
onClick={() => handleSymbolClick(char, symbol)}
257+
className="flex items-center justify-between p-3 border border-white/5 bg-black/20 rounded-sm hover:bg-emerald-500/10 hover:border-emerald-500/50 transition-all cursor-pointer group"
258+
>
259+
<span className="font-mono text-gray-400 group-hover:text-emerald-400 transition-colors">
260+
{char}
261+
</span>
262+
<span className="text-emerald-400 text-xl">{symbol}</span>
263+
</button>
264+
))}
265+
</div>
266+
</div>
267+
{/* About Section */}
268+
<div className="mt-12 border border-white/10 bg-white/[0.02] p-8 rounded-sm">
269+
<h3 className="font-playfairDisplay text-lg font-bold text-gray-300 mb-6 flex items-center gap-2">
270+
<NotebookIcon weight="fill" className="text-emerald-500" />
271+
About FezGlyph
272+
</h3>
273+
<div className="prose prose-invert max-w-none font-arvo">
274+
<p className="text-gray-400 font-light leading-relaxed mb-4">
275+
The symbols used in <strong>FezGlyph</strong> are technically
276+
known as <strong>Canadian Aboriginal Syllabics</strong>. Their
277+
inclusion here is purely for visual and aesthetic purposes within
278+
this digital art project, and we intend absolutely no disrespect
279+
toward the Indigenous communities, their histories, or the sacred
280+
nature of their scripts.
281+
</p>
282+
<ul className="list-disc pl-5 space-y-2 text-gray-400 font-light">
283+
<li>
284+
<strong>Origin:</strong> Developed in the 1840s by James Evans
285+
in collaboration with Indigenous speakers.
286+
</li>
287+
<li>
288+
<strong>Usage:</strong> A family of writing systems used by
289+
several Indigenous peoples in Canada (such as the Cree, Ojibwe,
290+
and Inuit).
291+
</li>
292+
<li>
293+
<strong>Context:</strong> While used here as a simple
294+
substitution cipher for their geometric aesthetic, in reality,
295+
they form a syllabary where each symbol represents a specific
296+
sound combination (like "ma", "ni", "po").
297+
</li>
298+
</ul>
299+
</div>
300+
</div>{' '}
301+
</div>
302+
</div>
303+
);
304+
};
305+
306+
export default FezGlyphPage;

0 commit comments

Comments
 (0)