style mostly
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
5d19b36fd30861387adb0c0008bfa71e6ac9099a- Parents
-
c2a21f5 - Tree
f12ffaa
5d19b36
5d19b36fd30861387adb0c0008bfa71e6ac9099ac2a21f5
f12ffaa| Status | File | + | - |
|---|---|---|---|
| M |
frontend/src/app/globals.css
|
5 | 0 |
| M |
frontend/src/app/layout.tsx
|
27 | 2 |
| M |
frontend/src/components/Game.tsx
|
12 | 12 |
| M |
frontend/src/components/TreeVisualizer.tsx
|
72 | 8 |
frontend/src/app/globals.cssmodified@@ -25,6 +25,11 @@ body { | ||
| 25 | 25 | font-family: Arial, Helvetica, sans-serif; |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | +/* Custom terminal font class */ | |
| 29 | +.font-terminal { | |
| 30 | + font-family: var(--font-terminal, 'Space Mono', 'Courier New', monospace); | |
| 31 | +} | |
| 32 | + | |
| 28 | 33 | /* Custom scrollbar for terminal */ |
| 29 | 34 | .scrollbar-thin { |
| 30 | 35 | scrollbar-width: thin; |
frontend/src/app/layout.tsxmodified@@ -1,13 +1,38 @@ | ||
| 1 | 1 | // src/app/layout.tsx |
| 2 | 2 | import type { Metadata } from "next"; |
| 3 | -import { Inter } from "next/font/google"; | |
| 3 | +import { Inter, Anonymous_Pro } from "next/font/google"; | |
| 4 | 4 | import "./globals.css"; |
| 5 | 5 | |
| 6 | 6 | const inter = Inter({ subsets: ["latin"] }); |
| 7 | +const spaceMono = Anonymous_Pro({ | |
| 8 | + subsets: ["latin"], | |
| 9 | + weight: ['400', '700'], | |
| 10 | + variable: '--font-terminal', | |
| 11 | +}); | |
| 7 | 12 | |
| 8 | 13 | export const metadata: Metadata = { |
| 9 | 14 | title: "Bashamole", |
| 10 | 15 | description: "A game to practice Unix navigation by hunting moles in the filesystem", |
| 16 | + icons: { | |
| 17 | + icon: [ | |
| 18 | + { url: '/favicon.ico' }, | |
| 19 | + { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' }, | |
| 20 | + { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, | |
| 21 | + ], | |
| 22 | + apple: [ | |
| 23 | + { url: '/apple-touch-icon.png' }, | |
| 24 | + ], | |
| 25 | + other: [ | |
| 26 | + { | |
| 27 | + rel: 'android-chrome-192x192', | |
| 28 | + url: '/android-chrome-192x192.png', | |
| 29 | + }, | |
| 30 | + { | |
| 31 | + rel: 'android-chrome-512x512', | |
| 32 | + url: '/android-chrome-512x512.png', | |
| 33 | + }, | |
| 34 | + ], | |
| 35 | + }, | |
| 11 | 36 | }; |
| 12 | 37 | |
| 13 | 38 | export default function RootLayout({ |
@@ -16,7 +41,7 @@ export default function RootLayout({ | ||
| 16 | 41 | children: React.ReactNode; |
| 17 | 42 | }>) { |
| 18 | 43 | return ( |
| 19 | - <html lang="en" className="h-full"> | |
| 44 | + <html lang="en" className={`h-full ${spaceMono.variable}`}> | |
| 20 | 45 | <body className={`${inter.className} h-full`}>{children}</body> |
| 21 | 46 | </html> |
| 22 | 47 | ); |
frontend/src/components/Game.tsxmodified@@ -316,12 +316,12 @@ const Game: React.FC = () => { | ||
| 316 | 316 | {!terminalMinimized && ( |
| 317 | 317 | <div |
| 318 | 318 | ref={terminalRef} |
| 319 | - className={`${terminalColors.content} p-4 font-mono text-base h-[350px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700`} | |
| 319 | + className={`${terminalColors.content} p-4 font-terminal text-base h-[350px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700`} | |
| 320 | 320 | onClick={() => inputRef.current?.focus()} |
| 321 | 321 | > |
| 322 | 322 | {commandHistory.map((entry, index) => ( |
| 323 | 323 | <div key={index} className="mb-1"> |
| 324 | - <div className="flex items-start font-mono"> | |
| 324 | + <div className="flex items-start font-terminal"> | |
| 325 | 325 | <span className="text-green-400">groundskeeper@molehill</span> |
| 326 | 326 | <span className="text-gray-400 mx-1">::</span> |
| 327 | 327 | <span className="text-blue-400">{entry.command.startsWith('Hunt started!') ? '~' : gameState.tree?.player_location || '~'}</span> |
@@ -331,7 +331,7 @@ const Game: React.FC = () => { | ||
| 331 | 331 | </span> |
| 332 | 332 | </div> |
| 333 | 333 | {entry.output && ( |
| 334 | - <div className={`${entry.success ? 'text-gray-300' : 'text-red-400'} ml-0 mt-1 font-mono whitespace-pre-wrap`}> | |
| 334 | + <div className={`${entry.success ? 'text-gray-300' : 'text-red-400'} ml-0 mt-1 font-terminal whitespace-pre-wrap`}> | |
| 335 | 335 | {entry.output.split('\n').map((line, i) => ( |
| 336 | 336 | <div key={i}>{line}</div> |
| 337 | 337 | ))} |
@@ -341,16 +341,16 @@ const Game: React.FC = () => { | ||
| 341 | 341 | ))} |
| 342 | 342 | |
| 343 | 343 | {/* Current input line */} |
| 344 | - <div className="flex items-start font-mono"> | |
| 344 | + <div className="flex items-start font-terminal"> | |
| 345 | 345 | <span className="text-green-400">groundskeeper@molehill</span> |
| 346 | 346 | <span className="text-gray-400 mx-1">::</span> |
| 347 | 347 | <span className="text-blue-400">{gameState.tree?.player_location || '~'}</span> |
| 348 | 348 | <span className="text-gray-400 ml-1">$</span> |
| 349 | 349 | <div className="flex-1 ml-2"> |
| 350 | 350 | <div className="relative inline-block"> |
| 351 | - <span className="text-gray-300 font-mono">{command}</span> | |
| 351 | + <span className="text-gray-300 font-terminal">{command}</span> | |
| 352 | 352 | <span |
| 353 | - className="text-gray-300 font-mono" | |
| 353 | + className="text-gray-300 font-terminal" | |
| 354 | 354 | style={{ |
| 355 | 355 | animation: 'blink 1s step-end infinite' |
| 356 | 356 | }} |
@@ -369,7 +369,7 @@ const Game: React.FC = () => { | ||
| 369 | 369 | } |
| 370 | 370 | }} |
| 371 | 371 | disabled={executing || gameState.tree?.is_completed} |
| 372 | - className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-mono" | |
| 372 | + className="absolute inset-0 w-full bg-transparent text-transparent outline-none caret-transparent font-terminal" | |
| 373 | 373 | placeholder="" |
| 374 | 374 | autoFocus |
| 375 | 375 | spellCheck={false} |
@@ -404,16 +404,16 @@ const Game: React.FC = () => { | ||
| 404 | 404 | <div className={`absolute bottom-0 left-0 right-0 ${isDarkMode ? 'bg-gray-900/90' : 'bg-white/90'} backdrop-blur-sm border-t ${isDarkMode ? 'border-gray-700' : 'border-gray-300'} p-4 z-20`}> |
| 405 | 405 | <div className="max-w-7xl mx-auto flex justify-between items-center"> |
| 406 | 406 | <div className="flex items-center gap-4"> |
| 407 | - <h1 className={`text-2xl font-bold ${isDarkMode ? 'text-white' : 'text-gray-900'}`}>Bashamole</h1> | |
| 408 | - <div className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}> | |
| 409 | - Location: <span className="font-mono text-blue-600 dark:text-blue-400">{gameState.tree.player_location}</span> | |
| 410 | - </div> | |
| 407 | + <h1 className={`text-2xl font-bold ${isDarkMode ? 'text-white' : 'text-gray-900'}`}><span className='font-terminal bg-gray-200 dark:bg-gray-700 text-red-900 dark:text-red-400 px-1 py-0 rounded'>bash</span>amole</h1> | |
| 408 | + {/* <div className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}> | |
| 409 | + Location: <span className="font-terminal text-blue-600 dark:text-blue-400">{gameState.tree.player_location}</span> | |
| 410 | + </div> */} | |
| 411 | 411 | </div> |
| 412 | 412 | |
| 413 | 413 | <div className="flex items-center gap-3"> |
| 414 | 414 | {gameState.tree.is_completed ? ( |
| 415 | 415 | <div className="text-green-600 dark:text-green-400 font-bold animate-pulse"> |
| 416 | - You found the mole! | |
| 416 | + You found a mole! | |
| 417 | 417 | </div> |
| 418 | 418 | ) : ( |
| 419 | 419 | <> |
frontend/src/components/TreeVisualizer.tsxmodified@@ -297,6 +297,43 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 297 | 297 | feMerge.append('feMergeNode').attr('in', 'coloredBlur'); |
| 298 | 298 | feMerge.append('feMergeNode').attr('in', 'SourceGraphic'); |
| 299 | 299 | |
| 300 | + // Add a subtle pulse animation for adjacent nodes | |
| 301 | + const pulseAnimation = defs.append('style') | |
| 302 | + .text(` | |
| 303 | + @keyframes subtlePulse { | |
| 304 | + 0%, 100% { opacity: 1; } | |
| 305 | + 50% { opacity: 0.8; } | |
| 306 | + } | |
| 307 | + .adjacent-node { | |
| 308 | + animation: subtlePulse 2s ease-in-out infinite; | |
| 309 | + } | |
| 310 | + `); | |
| 311 | + | |
| 312 | + // Helper function to check if a node is adjacent to the current location | |
| 313 | + const isAdjacentNode = (nodePath: string, currentPath: string): boolean => { | |
| 314 | + // Check if it's the parent directory | |
| 315 | + if (currentPath !== '/') { | |
| 316 | + const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/'; | |
| 317 | + if (nodePath === parentPath) return true; | |
| 318 | + } | |
| 319 | + | |
| 320 | + // Check if it's a direct child | |
| 321 | + if (currentPath === '/') { | |
| 322 | + // For root, children are paths with exactly one segment | |
| 323 | + const segments = nodePath.split('/').filter(s => s); | |
| 324 | + if (segments.length === 1) return true; | |
| 325 | + } else { | |
| 326 | + // For other directories, check if it's a direct child | |
| 327 | + if (nodePath.startsWith(currentPath + '/')) { | |
| 328 | + const relativePath = nodePath.substring(currentPath.length + 1); | |
| 329 | + // Make sure there are no additional slashes (not a grandchild) | |
| 330 | + if (!relativePath.includes('/')) return true; | |
| 331 | + } | |
| 332 | + } | |
| 333 | + | |
| 334 | + return false; | |
| 335 | + }; | |
| 336 | + | |
| 300 | 337 | // Add circles for nodes |
| 301 | 338 | node |
| 302 | 339 | .append('circle') |
@@ -317,15 +354,34 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 317 | 354 | return NODE_CONFIG.colors.regular.stroke; |
| 318 | 355 | }) |
| 319 | 356 | .style('stroke-width', NODE_CONFIG.strokeWidth.base) |
| 320 | - .style('cursor', 'pointer') | |
| 357 | + .style('cursor', d => { | |
| 358 | + // Only show pointer cursor for adjacent nodes | |
| 359 | + if (d.data.path === playerLocation) return 'default'; | |
| 360 | + return isAdjacentNode(d.data.path, playerLocation) ? 'pointer' : 'not-allowed'; | |
| 361 | + }) | |
| 321 | 362 | .style('filter', d => d.data.path === playerLocation ? NODE_CONFIG.glowFilter : 'none') |
| 363 | + .style('opacity', d => { | |
| 364 | + // Slightly fade non-adjacent nodes | |
| 365 | + if (d.data.path === playerLocation) return 1; | |
| 366 | + return isAdjacentNode(d.data.path, playerLocation) ? 1 : 0.6; | |
| 367 | + }) | |
| 322 | 368 | .style('transition', 'all 0.3s ease') |
| 369 | + .attr('class', d => { | |
| 370 | + // Add class for adjacent nodes to enable pulse animation | |
| 371 | + if (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) { | |
| 372 | + return 'adjacent-node'; | |
| 373 | + } | |
| 374 | + return ''; | |
| 375 | + }) | |
| 323 | 376 | .on('mouseover', function(event, d) { |
| 324 | - d3.select(this) | |
| 325 | - .transition() | |
| 326 | - .duration(ANIMATION_CONFIG.nodeHover.duration) | |
| 327 | - .attr('r', d.data.path === '/' ? NODE_CONFIG.sizes.root.hover : NODE_CONFIG.sizes.regular.hover) | |
| 328 | - .style('stroke-width', NODE_CONFIG.strokeWidth.hover); | |
| 377 | + // Only apply hover effect to adjacent nodes | |
| 378 | + if (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) { | |
| 379 | + d3.select(this) | |
| 380 | + .transition() | |
| 381 | + .duration(ANIMATION_CONFIG.nodeHover.duration) | |
| 382 | + .attr('r', d.data.path === '/' ? NODE_CONFIG.sizes.root.hover : NODE_CONFIG.sizes.regular.hover) | |
| 383 | + .style('stroke-width', NODE_CONFIG.strokeWidth.hover); | |
| 384 | + } | |
| 329 | 385 | }) |
| 330 | 386 | .on('mouseout', function(event, d) { |
| 331 | 387 | d3.select(this) |
@@ -337,7 +393,8 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 337 | 393 | .style('stroke-width', NODE_CONFIG.strokeWidth.base); |
| 338 | 394 | }) |
| 339 | 395 | .on('click', (event, d) => { |
| 340 | - if (onNodeClick) { | |
| 396 | + // Only allow clicks on adjacent nodes | |
| 397 | + if (onNodeClick && d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) { | |
| 341 | 398 | onNodeClick(d.data.path); |
| 342 | 399 | } |
| 343 | 400 | }); |
@@ -345,7 +402,14 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 345 | 402 | // Add tooltips |
| 346 | 403 | node |
| 347 | 404 | .append('title') |
| 348 | - .text(d => `${d.data.path}\n${d.data.description}\n${d.data.has_mole ? '🐭 Mole is here!' : ''}`); | |
| 405 | + .text(d => { | |
| 406 | + const baseText = `${d.data.path}\n${d.data.description}`; | |
| 407 | + const moleText = d.data.has_mole ? '\n🐭 Mole is here!' : ''; | |
| 408 | + const clickableText = (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) | |
| 409 | + ? '\n(Click to navigate here)' | |
| 410 | + : ''; | |
| 411 | + return baseText + moleText + clickableText; | |
| 412 | + }); | |
| 349 | 413 | |
| 350 | 414 | // Add labels |
| 351 | 415 | node |