|
| 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