👀
This commit is contained in:
31
package.json
31
package.json
@ -9,25 +9,28 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.1.1",
|
||||
"@astrojs/react": "^4.2.0",
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@tailwindcss/vite": "^4.0.8",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"astro": "^5.3.1",
|
||||
"@astrojs/node": "^9.2.1",
|
||||
"@astrojs/react": "^4.2.7",
|
||||
"@aws-sdk/client-s3": "^3.812.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/node": "^22.15.19",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"astro": "^5.7.13",
|
||||
"astro-robots": "^2.3.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.8",
|
||||
"typescript": "^5.7.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"daisyui": "5.0.0-beta.6"
|
||||
"daisyui": "5.0.35"
|
||||
}
|
||||
}
|
3210
pnpm-lock.yaml
generated
3210
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
365
src/components/Terminal.tsx
Normal file
365
src/components/Terminal.tsx
Normal file
@ -0,0 +1,365 @@
|
||||
import React, { useEffect, useRef, useMemo } from 'react';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
type FileSystemStructure = {
|
||||
[key: string]: string[] | string;
|
||||
};
|
||||
|
||||
interface TerminalProps {
|
||||
solution1: number;
|
||||
solution2: number;
|
||||
solution3: number;
|
||||
}
|
||||
|
||||
const TerminalComponent: React.FC<TerminalProps> = ({ solution1 = 42, solution2 = 87, solution3 = 5 }) => {
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const xtermRef = useRef<Terminal | null>(null);
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const commandHistory = useRef<string[]>([]);
|
||||
const historyIndex = useRef<number>(-1);
|
||||
const currentLine = useRef<string>('');
|
||||
|
||||
const fileSystem: FileSystemStructure = useMemo(() => ({
|
||||
'/': ['home', 'documents', 'etc', 'var', 'usr', 'README.txt', 'secret.txt'],
|
||||
'/home': ['user', 'guest'],
|
||||
'/home/user': ['puzzle_progress.txt'],
|
||||
'/home/guest': [],
|
||||
'/documents': ['report.doc', 'notes.txt'],
|
||||
'/etc': ['config', 'sys_data'],
|
||||
'/etc/config': ['network.conf', 'color_palette.config'],
|
||||
'/etc/sys_data': ['core_access.bin'],
|
||||
'/var': ['log', 'tmp'],
|
||||
'/var/log': ['system.log', 'critical_errors.log'],
|
||||
'/usr': ['bin', 'share'],
|
||||
'/usr/bin': ['common_commands.txt'],
|
||||
'/usr/share': ['docs', 'misc'],
|
||||
'/usr/share/docs': ['filesystem_layout.txt'],
|
||||
'/usr/share/misc': ['old_data.bak'],
|
||||
|
||||
// File Contents
|
||||
'/README.txt': "Welcome, challenger. Your mission, should you choose to accept it...\n" +
|
||||
"Is to find three numbers to unlock a combination lock.\n" +
|
||||
"These numbers are hidden within this simulated filesystem.\n" +
|
||||
"Use commands like 'ls', 'cd', and 'cat' to explore.\n" +
|
||||
"A good starting point might be to understand the lay of the land: 'cat /usr/share/docs/filesystem_layout.txt'\n" +
|
||||
"Good luck!",
|
||||
'/secret.txt': "SECURITY NOTICE:\n\nSystem access restricted to authorized personnel only.\nAll activities on this terminal are logged and monitored.\n\nNote to admins: The master backup codes have been moved to a more secure location.\nPlease refer to the new security protocol documentation.\n\nCheck system configuration in /etc and examine logs in /var/log if issues arise.",
|
||||
'/usr/share/docs/filesystem_layout.txt': "System Architecture Overview:\n" +
|
||||
"- /home: User-specific files.\n" +
|
||||
"- /etc: System configuration files and critical data.\n" +
|
||||
" |- /config: Display and network settings.\n" +
|
||||
" |- /sys_data: Core system binary data.\n" +
|
||||
"- /var: Variable data, such as logs and temporary files.\n" +
|
||||
" |- /log: System logs and error reports.\n" +
|
||||
"- /usr: System-wide shared resources and executables (read-only for you!).\n",
|
||||
|
||||
// Puzzle 1: Number Base
|
||||
'/etc/sys_data/core_access.bin': `System memory address reference:\n` +
|
||||
`Register B1: ${solution1.toString(2).padStart(8, '0')}\n` +
|
||||
`Note: Always decode binary values when transferring between subsystems.`,
|
||||
|
||||
// Puzzle 2: Hex Code (Color)
|
||||
'/etc/config/color_palette.config': `# Display Color Settings (RGB hex format)
|
||||
PRIMARY_COLOR=#FF${solution2.toString(16).padStart(2, '0')}33
|
||||
SECONDARY_COLOR=#33FF57
|
||||
ALERT_COLOR=#FF0000
|
||||
|
||||
# System Note:
|
||||
# Color channels must be isolated when importing to legacy devices.
|
||||
# Remember middle values carry essential security data.`,
|
||||
|
||||
// Puzzle 3: Log File Analysis
|
||||
'/var/log/critical_errors.log': "[ERROR 001] System boot failure sequence A.\n" +
|
||||
"[INFO 002] Retrying boot sequence B.\n" +
|
||||
"[ERROR 003] Disk read error on /dev/sda1.\n" +
|
||||
"[WARN 004] Network interface eth0 flapping.\n" +
|
||||
"[INFO 005] System recovered.\n" +
|
||||
"[DEBUG 006] User 'admin' login attempt.\n" +
|
||||
"[ERROR 007] Authentication failure for user 'admin'.\n" +
|
||||
"[INFO 008] User 'hacker' login successful.\n" +
|
||||
"[WARN 009] Unusual activity detected from IP 192.168.1.101.\n" +
|
||||
"[ERROR 010] Critical process 'security_daemon' terminated unexpectedly.\n" +
|
||||
"[INFO 011] Attempting to restart 'security_daemon'.\n" +
|
||||
`[ERROR 012] Failed to restart 'security_daemon'. Max retries (${solution3}) reached.\n` +
|
||||
"[ERROR 014] System shutting down due to critical errors.\n" +
|
||||
"[RECOVER] Backup facility requires retry count for authentication sequence.",
|
||||
|
||||
// Home directory files with hints
|
||||
'/home/user/puzzle_progress.txt': "My notes on the combination lock:\n- First number: __ (Check core system data)\n- Second number: __ (UI color settings have relevant info)\n- Third number: __ (Error logs contain critical values)\n\nReminder: Always convert between number formats as needed!",
|
||||
|
||||
// Document files with hints
|
||||
'/documents/report.doc': "Project Phoenix - Q3 Financial Report Summary\n\nStrictly Confidential\n\nNote: System core access requires binary authentication module as referenced in maintenance guide section 4.2.1",
|
||||
'/documents/notes.txt': "Grocery List:\n- Milk\n- Eggs\n- Bread\n- Mystery item #42 (NOT the combination number!)\n- Remember to check system logs for maintenance schedule\n\nReminders:\n- Review critical error logs for system failures\n- Update color palette settings per design team request",
|
||||
|
||||
// Network config with hint
|
||||
'/etc/config/network.conf': "HOSTNAME=ixabatasha-virtual-env\nIP_ADDR=10.1.33.7\nGATEWAY=10.1.33.1\nDNS_SERVER=8.8.8.8\n\n# Note: Display color palette in the same directory needs to be accessible for UI integration",
|
||||
|
||||
// System log with hint
|
||||
'/var/log/system.log': "[INFO] System startup sequence initiated.\n[INFO] All core services loaded successfully.\n[WARN] Low virtual memory. Consider rebooting non-essential modules.\n[INFO] User 'root' logged in from console.\n[NOTICE] Combination lock module initialized with secure parameters.\n[DEBUG] Numbers not stored in plaintext for security reasons.\n[INFO] Memory address values in binary format stored in /etc/sys_data\n[WARN] Check critical_errors.log for security daemon failures",
|
||||
|
||||
'/usr/bin/common_commands.txt': "Available commands: ls, cd, cat, clear, help (for README)\n\nImportant system paths:\n- Main config: /etc/config/\n- System data: /etc/sys_data/\n- Error logs: /var/log/",
|
||||
|
||||
'/usr/share/misc/old_data.bak': "ARCHIVE FILE: Contains outdated research notes from Project Chimera. Access restricted.\n\nPossible combination values tested:\n- 12-45-78 (Failed)\n- 24-68-99 (Failed)\n- 11-22-33 (Failed)\n\nNOTE: New secure generation algorithm implemented. Old combinations no longer valid.\n\nCurrent security relies on: core_access biometrics, color_palette UI safeguards, and error logging thresholds."
|
||||
}), [solution1, solution2, solution3]);
|
||||
const currentPath = useRef<string>('/');
|
||||
|
||||
const prompt = () => {
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.write(`\r\nroot@ixabatasha:${currentPath.current}# `);
|
||||
}
|
||||
};
|
||||
|
||||
const processCommand = (command: string) => {
|
||||
if (!xtermRef.current) return;
|
||||
const [cmd, ...args] = command.trim().split(' ');
|
||||
|
||||
switch (cmd) {
|
||||
case 'ls':
|
||||
const currentDirContent = fileSystem[currentPath.current];
|
||||
if (Array.isArray(currentDirContent)) {
|
||||
const displayItems = currentDirContent.map(item => {
|
||||
let itemFullPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + item;
|
||||
if (itemFullPath !== '/' && itemFullPath.endsWith('/')) itemFullPath = itemFullPath.slice(0, -1);
|
||||
|
||||
if (Array.isArray(fileSystem[itemFullPath])) {
|
||||
return item + '/';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
xtermRef.current.write('\r\n' + displayItems.join('\t'));
|
||||
} else {
|
||||
xtermRef.current.write(`\r\nls: cannot access '${currentPath.current}': Not a directory`);
|
||||
}
|
||||
break;
|
||||
case 'cd':
|
||||
const targetDir = args[0];
|
||||
if (!targetDir) {
|
||||
// cd to root
|
||||
currentPath.current = '/';
|
||||
} else if (targetDir === '..') {
|
||||
const parts = currentPath.current.split('/').filter(p => p);
|
||||
parts.pop();
|
||||
currentPath.current = '/' + parts.join('/');
|
||||
if (currentPath.current === '//') currentPath.current = '/';
|
||||
} else {
|
||||
let newPath: string;
|
||||
if (targetDir.startsWith('/')) {
|
||||
newPath = targetDir;
|
||||
} else {
|
||||
newPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + targetDir;
|
||||
}
|
||||
// Normalize path (remove trailing slashes unless it's the root)
|
||||
if (newPath !== '/' && newPath.endsWith('/')) {
|
||||
newPath = newPath.slice(0, -1);
|
||||
}
|
||||
|
||||
if (fileSystem[newPath] !== undefined) {
|
||||
if (Array.isArray(fileSystem[newPath])) {
|
||||
currentPath.current = newPath;
|
||||
} else {
|
||||
xtermRef.current.write(`\r\ncd: ${targetDir}: Not a directory`);
|
||||
}
|
||||
} else {
|
||||
xtermRef.current.write(`\r\ncd: ${targetDir}: No such file or directory`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'cat':
|
||||
const targetFileArg = args[0];
|
||||
if (!targetFileArg) {
|
||||
xtermRef.current.write('\r\ncat: missing file operand');
|
||||
break;
|
||||
}
|
||||
|
||||
let fullTargetPath: string;
|
||||
if (targetFileArg.startsWith('/')) {
|
||||
fullTargetPath = targetFileArg;
|
||||
} else {
|
||||
fullTargetPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + targetFileArg;
|
||||
}
|
||||
// Normalize path
|
||||
if (fullTargetPath !== '/' && fullTargetPath.endsWith('/')) {
|
||||
fullTargetPath = fullTargetPath.slice(0, -1);
|
||||
}
|
||||
|
||||
const fileContent = fileSystem[fullTargetPath];
|
||||
|
||||
if (fileContent === undefined) {
|
||||
xtermRef.current.write(`\r\ncat: ${targetFileArg}: No such file or directory`);
|
||||
} else if (Array.isArray(fileContent)) {
|
||||
xtermRef.current.write(`\r\ncat: ${targetFileArg}: Is a directory`);
|
||||
} else if (typeof fileContent === 'string') {
|
||||
xtermRef.current.write('\r\n' + fileContent.split('\n').join('\r\n'));
|
||||
} else {
|
||||
// Should not happen with current structure
|
||||
xtermRef.current.write(`\r\ncat: ${targetFileArg}: Cannot read this type of file`);
|
||||
}
|
||||
break;
|
||||
case 'clear':
|
||||
xtermRef.current.clear();
|
||||
break;
|
||||
case 'help':
|
||||
// Display available commands and tips
|
||||
const helpText =
|
||||
"Available Commands:\n" +
|
||||
" ls List directory contents\n" +
|
||||
" cd <directory> Change directory\n" +
|
||||
" cat <file> Display file contents\n" +
|
||||
" clear Clear the screen\n" +
|
||||
" help Display this help message\n\n" +
|
||||
"Tips:\n" +
|
||||
"- Use Tab for command/path completion\n" +
|
||||
"- Up/Down arrows navigate command history\n" +
|
||||
"- Type 'cat /usr/bin/common_commands.txt' for more system information\n" +
|
||||
"- Read the README.txt for mission details";
|
||||
|
||||
xtermRef.current.write('\r\n' + helpText);
|
||||
break;
|
||||
case '': // Handle empty command
|
||||
break;
|
||||
default:
|
||||
xtermRef.current.write(`\r\ncommand not found: ${cmd}\r\nTry 'help' for a list of available commands`);
|
||||
break;
|
||||
}
|
||||
prompt();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current && !xtermRef.current) {
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#d4d4d4',
|
||||
}
|
||||
});
|
||||
const fit = new FitAddon();
|
||||
const webLinks = new WebLinksAddon();
|
||||
|
||||
xtermRef.current = term;
|
||||
fitAddonRef.current = fit;
|
||||
|
||||
term.loadAddon(fit);
|
||||
term.loadAddon(webLinks);
|
||||
term.open(terminalRef.current);
|
||||
fit.fit();
|
||||
|
||||
term.write('Welcome to Ixabata-shell!\r\n');
|
||||
term.write('Type a command to begin. (Try ls, cd, cat)\r\n');
|
||||
prompt();
|
||||
|
||||
|
||||
term.onKey(({ key, domEvent }) => {
|
||||
if (!xtermRef.current) return;
|
||||
|
||||
const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;
|
||||
|
||||
if (domEvent.key === 'Enter') {
|
||||
if (currentLine.current.trim()) {
|
||||
commandHistory.current.push(currentLine.current);
|
||||
}
|
||||
historyIndex.current = commandHistory.current.length;
|
||||
processCommand(currentLine.current);
|
||||
currentLine.current = '';
|
||||
} else if (domEvent.key === 'Backspace') {
|
||||
if (currentLine.current.length > 0) {
|
||||
xtermRef.current.write('\b \b'); // Erase character
|
||||
currentLine.current = currentLine.current.slice(0, -1);
|
||||
}
|
||||
} else if (domEvent.key === 'ArrowUp') {
|
||||
if (commandHistory.current.length > 0 && historyIndex.current > 0) {
|
||||
historyIndex.current--;
|
||||
// Clear current line and write prompt + history item
|
||||
xtermRef.current.write('\x1b[2K\r');
|
||||
xtermRef.current.write(`root@ixabatasha:${currentPath.current}# `);
|
||||
xtermRef.current.write(commandHistory.current[historyIndex.current]);
|
||||
currentLine.current = commandHistory.current[historyIndex.current];
|
||||
}
|
||||
} else if (domEvent.key === 'ArrowDown') {
|
||||
if (commandHistory.current.length > 0 && historyIndex.current < commandHistory.current.length - 1) {
|
||||
historyIndex.current++;
|
||||
// Clear current line and write prompt + history item
|
||||
xtermRef.current.write('\x1b[2K\r');
|
||||
xtermRef.current.write(`root@ixabatasha:${currentPath.current}# `);
|
||||
xtermRef.current.write(commandHistory.current[historyIndex.current]);
|
||||
currentLine.current = commandHistory.current[historyIndex.current];
|
||||
} else if (historyIndex.current === commandHistory.current.length - 1) {
|
||||
historyIndex.current++;
|
||||
// Clear current line and write just the prompt
|
||||
xtermRef.current.write('\x1b[2K\r');
|
||||
xtermRef.current.write(`root@ixabatasha:${currentPath.current}# `);
|
||||
currentLine.current = "";
|
||||
}
|
||||
} else if (domEvent.key === 'Tab') {
|
||||
domEvent.preventDefault(); // Prevent default tab behavior (focus change)
|
||||
const inputParts = currentLine.current.split(' ');
|
||||
const currentArgument = inputParts.pop() || '';
|
||||
const commandPart = inputParts.join(' ');
|
||||
|
||||
let suggestions: string[] = [];
|
||||
const availableCommands = ['ls', 'cd', 'cat', 'clear', 'help'];
|
||||
|
||||
if (inputParts.length === 0 && !currentLine.current.includes(' ')) {
|
||||
suggestions = availableCommands.filter(c => c.startsWith(currentArgument));
|
||||
} else {
|
||||
const currentDirItems = Array.isArray(fileSystem[currentPath.current]) ? fileSystem[currentPath.current] as string[] : [];
|
||||
suggestions = currentDirItems.filter(item => item.startsWith(currentArgument));
|
||||
}
|
||||
|
||||
if (suggestions.length === 1) {
|
||||
const suggestion = suggestions[0];
|
||||
let resolvedSuggestionPath = suggestion;
|
||||
if (!suggestion.startsWith('/')) {
|
||||
resolvedSuggestionPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + suggestion;
|
||||
}
|
||||
if (resolvedSuggestionPath !== '/' && resolvedSuggestionPath.endsWith('/')) {
|
||||
resolvedSuggestionPath = resolvedSuggestionPath.slice(0, -1);
|
||||
}
|
||||
|
||||
const isDirectory = Array.isArray(fileSystem[resolvedSuggestionPath]);
|
||||
const completion = suggestion + (isDirectory ? '/' : ' ');
|
||||
|
||||
xtermRef.current.write(completion.substring(currentArgument.length));
|
||||
currentLine.current = (commandPart ? commandPart + ' ' : '') + completion;
|
||||
if (!isDirectory || !completion.endsWith('/')) {
|
||||
currentLine.current = currentLine.current.trimEnd();
|
||||
}
|
||||
|
||||
|
||||
} else if (suggestions.length > 1) {
|
||||
xtermRef.current.write('\r\n' + suggestions.join('\t') + '\r\n');
|
||||
prompt();
|
||||
xtermRef.current.write(currentLine.current);
|
||||
}
|
||||
// If no suggestions, do nothing (or beep: xtermRef.current.write('\x07');)
|
||||
|
||||
} else if (printable && key.length === 1) { // Ensure it's a single character, not a special key like 'Shift'
|
||||
xtermRef.current.write(key);
|
||||
currentLine.current += key;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle resize
|
||||
const handleResize = () => {
|
||||
fitAddonRef.current?.fit();
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
term.dispose();
|
||||
xtermRef.current = null;
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div ref={terminalRef} style={{ width: '100%', height: '100%' }} />;
|
||||
};
|
||||
|
||||
export default TerminalComponent;
|
57
src/pages/secret-terminal.astro
Normal file
57
src/pages/secret-terminal.astro
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
import TerminalComponent from '../components/Terminal.tsx';
|
||||
const title = "Secret Terminal";
|
||||
|
||||
/*
|
||||
Escape Room Terminal Puzzle
|
||||
==========================
|
||||
|
||||
This virtual Linux terminal contains three puzzles that reveal the
|
||||
combination lock code. Each number can be configured below.
|
||||
|
||||
Puzzle Locations & Solutions:
|
||||
|
||||
1. Binary Number Challenge
|
||||
- Location: /etc/sys_data/core_access.bin
|
||||
- How to find: Look for binary data in system files and convert to decimal
|
||||
|
||||
2. Hex Color Code Challenge
|
||||
- Location: /etc/config/color_palette.config
|
||||
- How to find: Extract the green component from a hex color code (FF**XX**33)
|
||||
and convert from hex to decimal
|
||||
|
||||
3. Log File Analysis Challenge
|
||||
- Location: /var/log/critical_errors.log
|
||||
- How to find: Look for the "max retries" count in the security_daemon error
|
||||
*/
|
||||
|
||||
// Define the solutions for the combination lock
|
||||
const solution1 = 29; // Binary puzzle solution
|
||||
const solution2 = 31; // Hex color code puzzle solution
|
||||
const solution3 = 5; // Log file analysis puzzle solution
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-full m-0 p-0 overflow-hidden">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤫</text></svg>"
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<link rel="stylesheet" href="/src/styles/global.css" />
|
||||
</head>
|
||||
<body class="h-full m-0 p-0 overflow-hidden bg-black">
|
||||
<div id="terminal-container" class="w-full h-full">
|
||||
<TerminalComponent
|
||||
client:only="react"
|
||||
solution1={solution1}
|
||||
solution2={solution2}
|
||||
solution3={solution3}
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user