Initial commit: Cyberpunk Dashboard

This commit is contained in:
2025-11-20 19:56:36 +09:00
commit af944216cc
45 changed files with 6721 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:18-alpine as builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3.8'
services:
dss-app:
container_name: dss_web
build: .
ports:
# СЛЕВА: Порт на твоем VPS (куда будет стучаться NPM)
# СПРАВА: Порт внутри контейнера (всегда 80, так как там Nginx)
- "4000:80"
restart: always

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dss-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

20
nginx.conf Normal file
View File

@@ -0,0 +1,20 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# Gzip сжатие
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# SPA Routing (Самое важное)
location / {
try_files $uri $uri/ /index.html;
}
# Кеш статики
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
}

4056
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "dss-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"tailwindcss": "^4.1.17"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

53
src/App.tsx Normal file
View File

@@ -0,0 +1,53 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Context Providers
import { AuthProvider } from './context/AuthContext';
import { ToastProvider } from './context/ToastContext';
import { SoundProvider } from './context/SoundContext'; // НОВОЕ
// Utils
import { ScrollToTop } from './components/utils/ScrollToTop';
// Layout Components
import { Layout } from './components/layout/Layout';
import { ProtectedRoute } from './components/layout/ProtectedRoute';
// Pages
import { Home } from './pages/Home';
import { AuthPage } from './pages/AuthPage';
import { Docs } from './pages/Docs';
import { Console } from './pages/Console';
import { Dashboard } from './pages/Dashboard';
import { Marketplace } from './pages/Marketplace';
import { Operations } from './pages/Operations';
import { Breach } from './pages/Breach'; // НОВОЕ
function App() {
return (
<AuthProvider>
<SoundProvider> {/* Оборачиваем в звук */}
<ToastProvider>
<Router>
<ScrollToTop />
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<AuthPage />} />
<Route path="/docs" element={<Docs />} />
<Route path="/console" element={<ProtectedRoute><Console /></ProtectedRoute>} />
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/market" element={<ProtectedRoute><Marketplace /></ProtectedRoute>} />
<Route path="/ops" element={<ProtectedRoute><Operations /></ProtectedRoute>} />
<Route path="/breach" element={<ProtectedRoute><Breach /></ProtectedRoute>} />
</Routes>
</Layout>
</Router>
</ToastProvider>
</SoundProvider>
</AuthProvider>
);
}
export default App;

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,74 @@
import React, { useState, useEffect, useRef } from 'react';
const LOG_MESSAGES = [
{ text: "User_882 breached firewall [Sector 4]", color: "text-green-500" },
{ text: "Connection lost: Node ALPHA-9", color: "text-red-500" },
{ text: "New contract available: DATA_HEIST", color: "text-indigo-400" },
{ text: "System purge initiated...", color: "text-yellow-500" },
{ text: "Decryption key found: 0x9928...", color: "text-white" },
{ text: "Proxy chain established", color: "text-gray-400" },
{ text: "Download complete: 42TB", color: "text-green-500" },
{ text: "Trace detected! Rerouting...", color: "text-red-400" }
];
export const LiveLog: React.FC = () => {
const [logs, setLogs] = useState<{id: number, text: string, color: string, time: string}[]>([]);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Генерируем начальные логи
const initial = Array.from({ length: 5 }).map((_, i) => ({
id: i,
text: LOG_MESSAGES[i % LOG_MESSAGES.length].text,
color: LOG_MESSAGES[i % LOG_MESSAGES.length].color,
time: new Date().toLocaleTimeString()
}));
setLogs(initial);
// Добавляем новые сообщения каждые 1.5 - 3 секунды
const interval = setInterval(() => {
const randomMsg = LOG_MESSAGES[Math.floor(Math.random() * LOG_MESSAGES.length)];
setLogs(prev => {
const newLogs = [...prev, {
id: Date.now(),
text: randomMsg.text,
color: randomMsg.color,
time: new Date().toLocaleTimeString()
}];
// Держим только последние 15 сообщений
return newLogs.slice(-15);
});
}, 2000);
return () => clearInterval(interval);
}, []);
// Автоскролл вниз
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
return (
<div className="bg-[#050505] border border-white/10 rounded h-full flex flex-col overflow-hidden">
<div className="p-3 border-b border-white/10 bg-white/5 flex items-center justify-between">
<span className="text-xs font-bold uppercase tracking-widest text-gray-400">Global Neural Feed</span>
<div className="flex gap-1">
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-ping"></div>
<div className="w-1.5 h-1.5 rounded-full bg-green-500"></div>
</div>
</div>
<div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-2 font-mono text-xs scrollbar-hide">
{logs.map((log) => (
<div key={log.id} className="flex gap-3 animate-fadeUp">
<span className="text-gray-600 shrink-0">[{log.time}]</span>
<span className={log.color}>{log.text}</span>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,55 @@
import React from 'react';
export const NeuralMap: React.FC = () => {
// Генерируем случайные точки
const nodes = Array.from({ length: 12 }).map((_, i) => ({
id: i,
top: `${Math.random() * 80 + 10}%`,
left: `${Math.random() * 80 + 10}%`,
status: Math.random() > 0.7 ? 'danger' : 'active',
pulse: Math.random() * 2 + 1
}));
return (
<div className="relative w-full h-[500px] bg-[#050505] border border-white/10 rounded overflow-hidden group">
{/* Сетка карты */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:40px_40px]"></div>
{/* Карта мира (схематично, картинкой) */}
<div className="absolute inset-0 opacity-20 bg-[url('https://upload.wikimedia.org/wikipedia/commons/8/80/World_map_-_low_resolution.svg')] bg-no-repeat bg-center bg-contain grayscale invert"></div>
{/* Сканирующий радар */}
<div className="absolute inset-0 bg-gradient-to-b from-indigo-500/5 to-transparent h-[10%] w-full animate-[scan_3s_linear_infinite] border-b border-indigo-500/20"></div>
{/* Узлы */}
{nodes.map((node) => (
<div
key={node.id}
className="absolute w-3 h-3 -ml-1.5 -mt-1.5 cursor-pointer hover:scale-150 transition-transform z-10"
style={{ top: node.top, left: node.left }}
>
<span className={`absolute inline-flex h-full w-full rounded-full opacity-75 animate-ping ${node.status === 'danger' ? 'bg-red-400' : 'bg-green-400'}`} style={{ animationDuration: `${node.pulse}s` }}></span>
<span className={`relative inline-flex rounded-full h-3 w-3 ${node.status === 'danger' ? 'bg-red-500' : 'bg-green-500'}`}></span>
{/* Tooltip при наведении */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-black border border-white/20 text-[10px] text-white px-2 py-1 whitespace-nowrap">
NODE_ID: {node.id}<br/>
STATUS: {node.status.toUpperCase()}
</div>
</div>
))}
{/* Статистика поверх карты */}
<div className="absolute bottom-4 left-4 bg-black/80 border border-white/10 p-3 backdrop-blur text-xs font-mono">
<div className="flex items-center gap-2 text-green-400">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
ACTIVE NODES: 8
</div>
<div className="flex items-center gap-2 text-red-400 mt-1">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
THREATS DETECTED: 4
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,128 @@
import React, { useState } from 'react';
import { Folder, FileText, Lock, ChevronRight, CornerDownRight, FileCode, Image } from 'lucide-react';
// Моковые данные файловой системы
const FILE_SYSTEM = {
root: [
{ id: 'classified', name: 'CLASSIFIED', type: 'folder', locked: true },
{ id: 'projects', name: 'PROJECT_OMEGA', type: 'folder', locked: false },
{ id: 'logs', name: 'SYSTEM_LOGS', type: 'folder', locked: false },
{ id: 'readme', name: 'READ_ME.txt', type: 'file', size: '2KB' },
],
projects: [
{ id: 'blueprint', name: 'blueprint_v4.pdf', type: 'file', size: '4.2MB' },
{ id: 'ai_core', name: 'ai_core_main.py', type: 'file', size: '128KB' },
],
logs: [
{ id: 'log1', name: 'access_attempt_failed.log', type: 'file', size: '4KB' },
{ id: 'log2', name: 'kernel_panic_dump.log', type: 'file', size: '24MB' },
]
};
export const TheVault: React.FC = () => {
const [currentPath, setCurrentPath] = useState<string[]>(['root']);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
// Получаем содержимое текущей папки
const currentFolderId = currentPath[currentPath.length - 1];
// @ts-ignore - простое демо, игнорируем строгую типизацию ключей для скорости
const items = FILE_SYSTEM[currentFolderId] || [];
const handleNavigate = (item: any) => {
if (item.type === 'folder') {
if (item.locked) {
alert('ACCESS DENIED: High clearance required.');
return;
}
setCurrentPath([...currentPath, item.id]);
} else {
setSelectedFile(item.name);
}
};
const goUp = () => {
if (currentPath.length > 1) {
setCurrentPath(currentPath.slice(0, -1));
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-[500px]">
{/* File Browser */}
<div className="lg:col-span-2 bg-[#0a0a0a] border border-white/10 rounded flex flex-col">
{/* Breadcrumbs */}
<div className="p-4 border-b border-white/10 flex items-center gap-2 text-xs font-mono text-gray-400 bg-white/5">
<span className="text-indigo-500">mnt</span>
{currentPath.map((p, i) => (
<React.Fragment key={i}>
<ChevronRight size={12} />
<span className={i === currentPath.length - 1 ? 'text-white' : 'cursor-pointer hover:text-white'}
onClick={() => setCurrentPath(currentPath.slice(0, i + 1))}>
{p.toUpperCase()}
</span>
</React.Fragment>
))}
</div>
{/* File List */}
<div className="flex-1 p-4 overflow-y-auto">
{currentPath.length > 1 && (
<div onClick={goUp} className="flex items-center gap-3 p-3 hover:bg-white/5 cursor-pointer text-gray-500 mb-2 border-b border-dashed border-white/10 pb-2">
<CornerDownRight className="rotate-180" size={16} />
<span>..</span>
</div>
)}
{items.map((item: any) => (
<div
key={item.id}
onClick={() => handleNavigate(item)}
className="group flex items-center justify-between p-3 hover:bg-indigo-500/10 cursor-pointer transition-colors border border-transparent hover:border-indigo-500/20 rounded mb-1"
>
<div className="flex items-center gap-3">
{item.type === 'folder' ? (
item.locked ? <Lock size={18} className="text-red-500" /> : <Folder size={18} className="text-indigo-400" />
) : (
<FileText size={18} className="text-gray-400 group-hover:text-white" />
)}
<span className={`text-sm font-mono ${item.locked ? 'text-gray-600' : 'text-gray-300 group-hover:text-white'}`}>
{item.name}
</span>
</div>
<div className="text-xs text-gray-600 font-mono">
{item.type === 'folder' ? 'DIR' : item.size}
</div>
</div>
))}
</div>
</div>
{/* Preview Panel */}
<div className="bg-[#0f0f0f] border border-white/10 rounded p-6 flex flex-col items-center justify-center text-center relative overflow-hidden">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/diagmonds-light.png')] opacity-5"></div>
{selectedFile ? (
<div className="animate-fadeUp w-full">
<FileCode size={48} className="mx-auto text-indigo-500 mb-4" />
<h3 className="text-white font-bold mb-2">{selectedFile}</h3>
<div className="w-full h-px bg-white/10 my-4"></div>
<div className="text-left text-xs font-mono text-gray-500 bg-black/50 p-4 rounded border border-white/5 h-48 overflow-hidden relative">
<div className="absolute top-2 right-2 w-2 h-2 rounded-full bg-green-500 animate-ping"></div>
<p>0101010100101 [ENCRYPTED CONTENT]</p>
<p>DECRYPTION KEY REQUIRED...</p>
<p className="text-indigo-400 mt-2">Download restricted.</p>
</div>
<button className="mt-6 w-full border border-indigo-500 text-indigo-500 py-2 text-xs uppercase tracking-widest hover:bg-indigo-500 hover:text-white transition-colors">
Decrpyt & Open
</button>
</div>
) : (
<div className="text-gray-600">
<Image size={48} className="mx-auto mb-4 opacity-20" />
<p className="text-sm">Select a file to preview content</p>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,9 @@
import React from 'react';
export const BackgroundEffects: React.FC = () => (
<>
<div className="fixed inset-0 bg-grid opacity-20 pointer-events-none z-0" />
<div className="scanline" />
<div className="fixed top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-indigo-600/10 blur-[120px] rounded-full pointer-events-none z-0" />
</>
);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Navbar } from './Navbar';
import { BackgroundEffects } from './BackgroundEffects';
interface LayoutProps {
children: React.ReactNode;
}
export const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-[#050505] text-[#e0e0e0] font-mono selection:bg-indigo-500/90 selection:text-white relative overflow-x-hidden">
<BackgroundEffects />
<Navbar />
<main className="pt-24 pb-20 px-6 max-w-7xl mx-auto relative z-10 min-h-screen flex flex-col">
{children}
</main>
<footer className="border-t border-white/10 bg-[#020202] py-8 relative z-10">
<div className="max-w-7xl mx-auto px-6 text-center text-gray-600 text-sm">
<p>© 2025 DIMEDROL SYSTEMS. ALL RIGHTS RESERVED.</p>
<p className="text-xs mt-2">EST. 199X // SECURE CONNECTION</p>
</div>
</footer>
</div>
);
};

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Terminal, ChevronRight, LogOut, User, Wifi, Gamepad2 } from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { useSound } from '../../context/SoundContext';
interface NavLinkProps {
to: string;
label: string;
}
const NavLink: React.FC<NavLinkProps> = ({ to, label }) => {
const location = useLocation();
const isActive = location.pathname === to;
const { playHover, playClick } = useSound();
return (
<Link
to={to}
onMouseEnter={playHover}
onClick={playClick}
className={`flex items-center gap-2 text-sm uppercase tracking-wider transition-colors
${isActive ? 'text-indigo-400' : 'text-gray-400 hover:text-white'}`}
>
{isActive && <ChevronRight size={14} className="animate-pulse" />}
{label}
</Link>
);
};
export const Navbar: React.FC = () => {
const { user, isAuthenticated, logout } = useAuth();
const { playClick } = useSound();
const navigate = useNavigate();
const handleLogout = () => {
playClick();
logout();
navigate('/');
};
return (
<header className="fixed top-0 w-full z-40 border-b border-white/10 bg-[#050505]/90 backdrop-blur-md">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<Link to="/" onClick={playClick} className="flex items-center gap-2 text-indigo-400 group">
<Terminal size={20} className="group-hover:rotate-12 transition-transform" />
<span className="font-bold tracking-widest text-white group-hover:text-indigo-400 transition-colors hidden md:inline">
DIMEDROL_SYSTEMS
</span>
<span className="font-bold tracking-widest text-white md:hidden">DSS</span>
</Link>
<nav className="hidden md:flex items-center gap-8">
<NavLink to="/" label="Home" />
{isAuthenticated && <NavLink to="/dashboard" label="Dashboard" />}
{isAuthenticated && <NavLink to="/market" label="Market" />}
{isAuthenticated && <NavLink to="/ops" label="Operations" />}
{isAuthenticated && <NavLink to="/breach" label="Breach" />}
<NavLink to="/console" label="Terminal" />
</nav>
<div className="flex items-center gap-4 md:gap-6 text-xs">
<div className="hidden md:flex items-center gap-2 text-green-500 border-r border-white/10 pr-6">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
</span>
<span className="tracking-widest font-bold">ONLINE</span>
</div>
{isAuthenticated && user ? (
<div className="flex items-center gap-4">
<div className="flex flex-col items-end">
<span className="text-gray-400 flex items-center gap-2 uppercase">
<User size={12} className="text-indigo-500"/> {user.username}
</span>
</div>
<button
onClick={handleLogout}
className="text-gray-500 hover:text-red-400 transition-colors"
title="Disconnect"
>
<LogOut size={18} />
</button>
</div>
) : (
<Link to="/login" onClick={playClick}>
<button className="group flex items-center gap-2 border border-white/20 hover:border-indigo-500/50 hover:bg-indigo-500/10 px-4 py-1.5 uppercase transition-all text-white tracking-widest">
<Wifi size={14} className="text-gray-500 group-hover:text-indigo-400" />
Login
</button>
</Link>
)}
</div>
</div>
</header>
);
};

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <div className="min-h-screen flex items-center justify-center text-indigo-500">LOADING PROTOCOLS...</div>;
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { useSound } from '../../context/SoundContext';
interface CyberButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick?: () => void;
disabled?: boolean;
}
export const CyberButton: React.FC<CyberButtonProps> = ({
children,
variant = 'primary',
onClick,
disabled
}) => {
const isPrimary = variant === 'primary';
const { playClick, playHover } = useSound();
return (
<button
onClick={() => {
if (!disabled) {
playClick();
onClick && onClick();
}
}}
onMouseEnter={() => !disabled && playHover()}
disabled={disabled}
className={`group relative px-8 py-3 font-bold uppercase tracking-widest overflow-hidden transition-all
${isPrimary ? 'bg-white text-black' : 'border border-white/20 text-gray-400 hover:text-white'}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
>
{isPrimary && !disabled && (
<div className="absolute inset-0 w-full h-full bg-indigo-600 translate-y-full group-hover:translate-y-0 transition-transform duration-300" />
)}
<span className={`relative flex items-center gap-2 ${isPrimary ? 'group-hover:text-white' : ''} transition-colors`}>
{children}
</span>
</button>
);
};

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Lock, User } from 'lucide-react';
interface CyberInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
icon?: 'user' | 'lock';
error?: string;
}
export const CyberInput: React.FC<CyberInputProps> = ({ label, icon, error, ...props }) => {
return (
<div className="w-full mb-4">
<label className="block text-xs uppercase tracking-widest text-gray-500 mb-2 font-bold">
{label}
</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500 group-focus-within:text-indigo-400 transition-colors">
{icon === 'user' && <User size={18} />}
{icon === 'lock' && <Lock size={18} />}
</div>
<input
{...props}
className="w-full bg-[#0f0f0f] border border-white/10 text-white pl-10 pr-4 py-3 focus:outline-none focus:border-indigo-500 focus:bg-indigo-500/5 transition-all font-mono placeholder-gray-700"
/>
{/* Декоративный уголок */}
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-white/20 group-focus-within:border-indigo-500 transition-colors"></div>
</div>
{/* ИСПРАВЛЕНИЕ ЗДЕСЬ: заменили > на &gt; */}
{error && (
<p className="text-red-500 text-xs mt-1 animate-pulse">
&gt; ERROR: {error}
</p>
)}
</div>
);
};

View File

@@ -0,0 +1,95 @@
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; // <--- Импортируем портал
import { X, AlertTriangle, CheckCircle, Info } from 'lucide-react';
interface CyberModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
children: React.ReactNode;
type?: 'info' | 'danger' | 'success';
}
export const CyberModal: React.FC<CyberModalProps> = ({
isOpen, onClose, onConfirm, title, children, type = 'info'
}) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
// Блокируем скролл основного сайта, когда открыта модалка
if (isOpen) {
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
if (!isOpen || !mounted) return null;
// Используем createPortal, чтобы рендерить модалку ПРЯМО в body,
// минуя ограничения max-w-7xl в Layout
return createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
{/* Backdrop (Фон) - теперь он точно на весь экран */}
<div
className="absolute inset-0 bg-black/80 backdrop-blur-sm animate-fadeIn"
onClick={onClose}
></div>
{/* Modal Content */}
<div className="relative w-full max-w-md bg-[#0a0a0a] border border-white/10 shadow-2xl animate-fadeUp z-10">
{/* Цветная полоска сверху */}
<div className={`h-1 w-full ${
type === 'danger' ? 'bg-red-500' :
type === 'success' ? 'bg-green-500' : 'bg-indigo-500'
}`}></div>
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-white flex items-center gap-2 uppercase tracking-widest">
{type === 'danger' && <AlertTriangle className="text-red-500" size={20} />}
{type === 'success' && <CheckCircle className="text-green-500" size={20} />}
{type === 'info' && <Info className="text-indigo-500" size={20} />}
{title}
</h3>
<button onClick={onClose} className="text-gray-500 hover:text-white transition-colors">
<X size={20} />
</button>
</div>
<div className="text-gray-300 mb-8 leading-relaxed">
{children}
</div>
<div className="flex gap-4">
<button
onClick={onClose}
className="flex-1 py-3 border border-white/10 text-gray-400 hover:bg-white/5 uppercase tracking-widest text-sm font-bold transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className={`flex-1 py-3 uppercase tracking-widest text-sm font-bold text-black transition-colors ${
type === 'danger' ? 'bg-red-500 hover:bg-red-400' :
type === 'success' ? 'bg-green-500 hover:bg-green-400' : 'bg-indigo-500 hover:bg-indigo-400'
}`}
>
Confirm
</button>
</div>
</div>
{/* Декор углов */}
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-white/30"></div>
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-white/30"></div>
</div>
</div>,
document.body // <--- Второй аргумент: куда вставлять (в body)
);
};

View File

@@ -0,0 +1,41 @@
import React from 'react';
interface Tab {
id: string;
label: string;
icon?: React.ReactNode;
}
interface CyberTabsProps {
tabs: Tab[];
activeTab: string;
onChange: (id: string) => void;
}
export const CyberTabs: React.FC<CyberTabsProps> = ({ tabs, activeTab, onChange }) => {
return (
<div className="flex border-b border-white/10 mb-6 overflow-x-auto scrollbar-hide">
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => onChange(tab.id)}
className={`
group relative px-6 py-3 flex items-center gap-2 text-sm font-bold tracking-widest uppercase transition-all
${isActive ? 'text-white bg-white/5' : 'text-gray-500 hover:text-gray-300 hover:bg-white/5'}
`}
>
{/* Активная линия сверху */}
{isActive && <div className="absolute top-0 left-0 w-full h-[2px] bg-indigo-500 shadow-[0_0_10px_rgba(99,102,241,0.5)]"></div>}
<span className={isActive ? 'text-indigo-400' : 'group-hover:text-white transition-colors'}>
{tab.icon}
</span>
{tab.label}
</button>
);
})}
</div>
);
};

View File

@@ -0,0 +1,24 @@
import React from 'react';
interface FeatureCardProps {
icon: React.ReactNode;
title: string;
desc: string;
}
export const FeatureCard: React.FC<FeatureCardProps> = ({ icon, title, desc }) => (
<div className="group relative p-8 border border-white/10 bg-[#0a0a0a] hover:border-indigo-500/50 transition-all duration-300 hover:-translate-y-1">
<div className="absolute top-0 right-0 w-0 h-0 border-t-[30px] border-r-[30px] border-t-transparent border-r-white/5 group-hover:border-r-indigo-500 transition-all"></div>
<div className="text-indigo-500 mb-6 group-hover:text-white transition-colors">
{icon}
</div>
<h3 className="text-xl font-bold mb-4 flex items-center gap-2 text-white group-hover:text-indigo-300">
{title}
</h3>
<p className="text-gray-400 text-sm leading-relaxed">
{desc}
</p>
</div>
);

View File

@@ -0,0 +1,17 @@
import React from 'react';
interface StatItemProps {
label: string;
value: string;
}
export const StatItem: React.FC<StatItemProps> = ({ label, value }) => (
<div className="p-6 bg-[#0a0a0a] border-r border-white/5 last:border-r-0 hover:bg-[#111] transition-colors group">
<div className="text-xs uppercase tracking-widest text-gray-500 mb-2 group-hover:text-indigo-400 transition-colors">
{label}
</div>
<div className="text-2xl font-bold text-white">
{value}
</div>
</div>
);

View File

@@ -0,0 +1,12 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export const ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
};

View File

@@ -0,0 +1,90 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
interface User {
username: string;
role: 'ADMIN' | 'USER';
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
register: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Проверка при загрузке страницы
useEffect(() => {
const storedUser = localStorage.getItem('dss_user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
setIsLoading(false);
}, []);
const login = async (username: string, password: string) => {
// Имитация запроса к серверу
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
const usersDb = JSON.parse(localStorage.getItem('dss_users_db') || '[]');
const foundUser = usersDb.find((u: any) => u.username === username && u.password === password);
if (foundUser) {
const userData: User = { username: foundUser.username, role: 'USER' };
setUser(userData);
localStorage.setItem('dss_user', JSON.stringify(userData));
resolve();
} else {
reject(new Error('ACCESS DENIED: Invalid credentials'));
}
}, 1000);
});
};
const register = async (username: string, password: string) => {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
const usersDb = JSON.parse(localStorage.getItem('dss_users_db') || '[]');
if (usersDb.find((u: any) => u.username === username)) {
reject(new Error('USER ALREADY EXISTS'));
return;
}
const newUser = { username, password };
usersDb.push(newUser);
localStorage.setItem('dss_users_db', JSON.stringify(usersDb));
// Автоматический вход после регистрации
const userData: User = { username, role: 'USER' };
setUser(userData);
localStorage.setItem('dss_user', JSON.stringify(userData));
resolve();
}, 1000);
});
};
const logout = () => {
setUser(null);
localStorage.removeItem('dss_user');
};
return (
<AuthContext.Provider value={{ user, isAuthenticated: !!user, isLoading, login, register, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within an AuthProvider');
return context;
};

View File

@@ -0,0 +1,98 @@
import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
import { Volume2, VolumeX } from 'lucide-react';
interface SoundContextType {
playClick: () => void;
playHover: () => void;
playSuccess: () => void;
playError: () => void;
playTyping: () => void;
toggleMute: () => void;
isMuted: boolean;
}
const SoundContext = createContext<SoundContextType | undefined>(undefined);
export const SoundProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isMuted, setIsMuted] = useState(false);
const audioCtxRef = useRef<AudioContext | null>(null);
const getContext = () => {
if (!audioCtxRef.current) {
// @ts-ignore
const Ctx = window.AudioContext || window.webkitAudioContext;
audioCtxRef.current = new Ctx();
}
return audioCtxRef.current;
};
const playTone = (freq: number, type: OscillatorType, duration: number, vol: number = 0.1) => {
if (isMuted) return;
try {
const ctx = getContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gain.gain.setValueAtTime(vol, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + duration);
} catch (e) {
console.error("Audio API error", e);
}
};
const playClick = useCallback(() => {
playTone(1200, 'sine', 0.05, 0.05);
}, [isMuted]);
const playHover = useCallback(() => {
if (Math.random() > 0.5) playTone(800, 'triangle', 0.01, 0.005);
}, [isMuted]);
const playSuccess = useCallback(() => {
if (isMuted) return;
setTimeout(() => playTone(880, 'sine', 0.2, 0.1), 0);
setTimeout(() => playTone(1108, 'sine', 0.2, 0.1), 100);
setTimeout(() => playTone(1318, 'sine', 0.4, 0.1), 200);
}, [isMuted]);
const playError = useCallback(() => {
if (isMuted) return;
playTone(150, 'sawtooth', 0.3, 0.1);
setTimeout(() => playTone(100, 'sawtooth', 0.3, 0.1), 100);
}, [isMuted]);
const playTyping = useCallback(() => {
playTone(600 + Math.random() * 200, 'square', 0.02, 0.01);
}, [isMuted]);
const toggleMute = () => setIsMuted(!isMuted);
return (
<SoundContext.Provider value={{ playClick, playHover, playSuccess, playError, playTyping, toggleMute, isMuted }}>
{children}
<div className="fixed bottom-6 left-6 z-[100]">
<button
onClick={toggleMute}
className="p-2 bg-black/50 border border-white/10 rounded-full text-gray-500 hover:text-white hover:border-white/30 transition-all backdrop-blur"
>
{isMuted ? <VolumeX size={16} /> : <Volume2 size={16} />}
</button>
</div>
</SoundContext.Provider>
);
};
export const useSound = () => {
const context = useContext(SoundContext);
if (!context) throw new Error('useSound must be used within SoundProvider');
return context;
};

View File

@@ -0,0 +1,75 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import { X, AlertCircle, CheckCircle, Info } from 'lucide-react';
type ToastType = 'success' | 'error' | 'info';
interface Toast {
id: number;
message: string;
type: ToastType;
}
interface ToastContextType {
addToast: (message: string, type?: ToastType) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((message: string, type: ToastType = 'info') => {
const id = Date.now();
setToasts((prev) => [...prev, { id, message, type }]);
// Автоудаление через 3 секунды
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
const removeToast = (id: number) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
};
return (
<ToastContext.Provider value={{ addToast }}>
{children}
{/* Рендер уведомлений (Fixed Container) */}
<div className="fixed bottom-8 right-8 z-50 flex flex-col gap-2 pointer-events-none">
{toasts.map((toast) => (
<div
key={toast.id}
className={`
pointer-events-auto w-80 p-4 border-l-4 bg-[#0a0a0a] text-white shadow-2xl
flex items-start gap-3 transition-all animate-slideIn
${toast.type === 'success' ? 'border-green-500 shadow-green-500/10' : ''}
${toast.type === 'error' ? 'border-red-500 shadow-red-500/10' : ''}
${toast.type === 'info' ? 'border-indigo-500 shadow-indigo-500/10' : ''}
`}
>
<div className="mt-0.5">
{toast.type === 'success' && <CheckCircle size={16} className="text-green-500" />}
{toast.type === 'error' && <AlertCircle size={16} className="text-red-500" />}
{toast.type === 'info' && <Info size={16} className="text-indigo-500" />}
</div>
<div className="flex-1">
<h4 className="font-bold text-xs uppercase tracking-widest mb-1 opacity-50">System Notification</h4>
<p className="text-sm font-mono leading-tight">{toast.message}</p>
</div>
<button onClick={() => removeToast(toast.id)} className="text-gray-600 hover:text-white">
<X size={14} />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within a ToastProvider');
return context;
};

View File

@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react';
export const useTypewriter = (text: string, speed: number = 50): string => {
const [displayText, setDisplayText] = useState<string>('');
useEffect(() => {
let index = 0;
const interval = setInterval(() => {
setDisplayText(text.slice(0, index));
index++;
if (index > text.length) clearInterval(interval);
}, speed);
return () => clearInterval(interval);
}, [text, speed]);
return displayText;
};

85
src/index.css Normal file
View File

@@ -0,0 +1,85 @@
@import "tailwindcss";
@layer utilities {
.bg-grid {
background-size: 40px 40px;
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
}
.scanline {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(255,255,255,0),
rgba(255,255,255,0) 50%,
rgba(0,0,0,0.2) 50%,
rgba(0,0,0,0.2)
);
background-size: 100% 4px;
pointer-events: none; /* ВАЖНО: чтобы клики проходили сквозь полоски */
/* ИЗМЕНЕНИЕ ЗДЕСЬ: Было 50, ставим максимум, чтобы быть поверх модалок */
z-index: 9999;
}
.cursor-blink {
animation: blink 1s step-end infinite;
}
/* Анимация появления страницы */
.page-enter {
animation: fadeUp 0.5s ease-out forwards;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.animate-slideIn {
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes scan {
0% { top: -10%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
@layer utilities {
/* Эффект свечения при наведении на легендарный предмет */
.glow-legendary:hover {
box-shadow: 0 0 20px rgba(234, 179, 8, 0.2);
}
}
/* Кастомный скроллбар */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #050505;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4f46e5;
}
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

130
src/pages/AuthPage.tsx Normal file
View File

@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../context/ToastContext';
import { CyberInput } from '../components/ui/CyberInput';
import { CyberButton } from '../components/ui/CyberButton';
import { ShieldCheck, AlertTriangle, Cpu } from 'lucide-react';
export const AuthPage: React.FC = () => {
const [isLogin, setIsLogin] = useState(true);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login, register } = useAuth();
const { addToast } = useToast();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
if (isLogin) {
await login(username, password);
addToast(`ACCESS GRANTED. Welcome back, ${username}.`, 'success');
} else {
await register(username, password);
addToast(`NEW NODE REGISTERED. ID: ${username}`, 'success');
}
// Перенаправляем на дашборд после успеха
navigate('/dashboard');
} catch (err: any) {
const errorMessage = err.message || 'Unknown System Error';
setError(errorMessage);
addToast(errorMessage, 'error');
} finally {
setLoading(false);
}
};
return (
<div className="page-enter min-h-[70vh] flex items-center justify-center relative">
{/* Декоративный фон позади формы */}
<div className="absolute w-full max-w-lg h-full border-x border-white/5 pointer-events-none"></div>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-indigo-600/5 blur-[100px] rounded-full pointer-events-none"></div>
<div className="w-full max-w-md bg-[#0a0a0a] border border-white/10 p-8 relative z-10 shadow-2xl shadow-black/50">
{/* Декоративные уголки */}
<div className="absolute top-0 left-0 w-2 h-2 border-t border-l border-white/20"></div>
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-white/20"></div>
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-white/20"></div>
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-white/20"></div>
{/* Заголовок */}
<div className="text-center mb-8">
<div className={`inline-flex p-3 rounded-full mb-4 border transition-all duration-500 ${
isLogin
? 'bg-indigo-500/10 border-indigo-500/20 text-indigo-500'
: 'bg-purple-500/10 border-purple-500/20 text-purple-500'
}`}>
{isLogin ? <ShieldCheck size={32} /> : <Cpu size={32} />}
</div>
<h2 className="text-2xl font-bold text-white tracking-tight">
{isLogin ? 'SYSTEM ACCESS' : 'NEW PROTOCOL'}
</h2>
<p className="text-gray-500 text-xs uppercase tracking-widest mt-2 flex items-center justify-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full ${isLogin ? 'bg-green-500 animate-pulse' : 'bg-purple-500'}`}></span>
{isLogin ? 'Identify yourself' : 'Register new node'}
</p>
</div>
{/* Форма */}
<form onSubmit={handleSubmit}>
<CyberInput
label="Username"
icon="user"
placeholder="codename"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<CyberInput
label="Password"
type="password"
icon="lock"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{/* Блок ошибки */}
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/20 text-red-400 text-xs font-mono flex items-start gap-2 animate-pulse">
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
<span>ERROR: {error}</span>
</div>
)}
<div className="mt-8">
<CyberButton variant="primary" disabled={loading}>
{loading ? 'PROCESSING...' : (isLogin ? 'AUTHENTICATE' : 'INITIALIZE')}
</CyberButton>
</div>
</form>
{/* Переключатель */}
<div className="mt-6 text-center text-sm text-gray-400 border-t border-white/5 pt-6">
{isLogin ? "No clearance?" : "Already operative?"}{" "}
<button
onClick={() => {
setIsLogin(!isLogin);
setError('');
setUsername('');
setPassword('');
}}
className="text-indigo-400 hover:text-indigo-300 underline decoration-indigo-500/30 underline-offset-4 uppercase text-xs font-bold tracking-wide ml-2"
>
{isLogin ? "Request Access" : "System Login"}
</button>
</div>
</div>
</div>
);
};

197
src/pages/Breach.tsx Normal file
View File

@@ -0,0 +1,197 @@
import React, { useState, useEffect } from 'react';
import { useToast } from '../context/ToastContext';
import { useSound } from '../context/SoundContext';
import { Cpu, RefreshCw } from 'lucide-react';
const MATRIX_SIZE = 5;
const HEX_CHARS = ['1C', 'BD', '55', 'E9', '7A'];
const BUFFER_SIZE = 4;
export const Breach: React.FC = () => {
const [matrix, setMatrix] = useState<string[][]>([]);
const [targetSequence, setTargetSequence] = useState<string[]>([]);
const [buffer, setBuffer] = useState<string[]>([]);
const [currentRow, setCurrentRow] = useState(0);
const [currentCol, setCurrentCol] = useState(0);
const [isRowActive, setIsRowActive] = useState(true);
const [gameState, setGameState] = useState<'playing' | 'won' | 'lost'>('playing');
const [timeLeft, setTimeLeft] = useState(30);
const { addToast } = useToast();
const { playClick, playHover, playSuccess, playError } = useSound();
const initGame = () => {
const newMatrix = Array(MATRIX_SIZE).fill(null).map(() =>
Array(MATRIX_SIZE).fill(null).map(() =>
HEX_CHARS[Math.floor(Math.random() * HEX_CHARS.length)]
)
);
const newTarget = Array(3).fill(null).map(() =>
HEX_CHARS[Math.floor(Math.random() * HEX_CHARS.length)]
);
setMatrix(newMatrix);
setTargetSequence(newTarget);
setBuffer([]);
setCurrentRow(0);
setCurrentCol(0);
setIsRowActive(true);
setGameState('playing');
setTimeLeft(30);
};
useEffect(() => {
initGame();
}, []);
useEffect(() => {
if (gameState !== 'playing') return;
const timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
setGameState('lost');
playError();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [gameState, playError]);
const handleCellClick = (row: number, col: number, value: string) => {
if (gameState !== 'playing') return;
const isValidMove = isRowActive ? row === currentRow : col === currentCol;
if (isValidMove) {
playClick();
const newBuffer = [...buffer, value];
setBuffer(newBuffer);
if (isRowActive) {
setCurrentCol(col);
} else {
setCurrentRow(row);
}
setIsRowActive(!isRowActive);
checkWinCondition(newBuffer);
} else {
playError();
}
};
const checkWinCondition = (currentBuffer: string[]) => {
const bufferStr = currentBuffer.join('');
const targetStr = targetSequence.join('');
if (bufferStr.includes(targetStr)) {
setGameState('won');
playSuccess();
addToast("SYSTEM BREACHED. ACCESS GRANTED.", "success");
} else if (currentBuffer.length >= BUFFER_SIZE) {
setGameState('lost');
playError();
addToast("BREACH FAILED. BUFFER OVERFLOW.", "error");
}
};
return (
<div className="page-enter min-h-[80vh] flex flex-col items-center justify-center">
<div className="w-full max-w-4xl grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="bg-[#0a0a0a] border border-white/10 p-8 rounded-lg relative overflow-hidden">
{gameState === 'lost' && <div className="absolute inset-0 bg-red-500/10 z-10 pointer-events-none animate-pulse"></div>}
{gameState === 'won' && <div className="absolute inset-0 bg-green-500/10 z-10 pointer-events-none"></div>}
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Cpu className="text-indigo-500" /> BREACH PROTOCOL
</h2>
<div className={`font-mono text-xl font-bold ${timeLeft < 10 ? 'text-red-500' : 'text-white'}`}>
00:{timeLeft < 10 ? `0${timeLeft}` : timeLeft}
</div>
</div>
<div className="grid grid-cols-5 gap-2">
{matrix.map((row, rIndex) => (
row.map((cell, cIndex) => {
const isActiveAxis = isRowActive ? rIndex === currentRow : cIndex === currentCol;
return (
<button
key={`${rIndex}-${cIndex}`}
onMouseEnter={playHover}
onClick={() => handleCellClick(rIndex, cIndex, cell)}
disabled={!isActiveAxis || gameState !== 'playing'}
className={`
h-12 w-12 flex items-center justify-center font-mono text-lg font-bold transition-all duration-200
${isActiveAxis && gameState === 'playing'
? 'bg-indigo-500/20 border border-indigo-500 text-white hover:bg-indigo-500 hover:shadow-[0_0_15px_rgba(99,102,241,0.5)]'
: 'bg-white/5 border border-transparent text-gray-600'}
`}
>
{cell}
</button>
);
})
))}
</div>
</div>
<div className="flex flex-col gap-6">
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg">
<h3 className="text-sm text-gray-500 uppercase tracking-widest mb-4">Target Sequence</h3>
<div className="flex gap-2">
{targetSequence.map((code, i) => (
<div key={i} className="w-12 h-12 border border-white/20 flex items-center justify-center text-white font-mono font-bold text-lg">
{code}
</div>
))}
</div>
</div>
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg flex-1">
<h3 className="text-sm text-gray-500 uppercase tracking-widest mb-4">Buffer RAM</h3>
<div className="flex gap-2 mb-8">
{Array(BUFFER_SIZE).fill(null).map((_, i) => (
<div key={i} className={`
w-12 h-12 border border-dashed flex items-center justify-center font-mono text-lg
${buffer[i] ? 'border-indigo-500 text-indigo-400 bg-indigo-500/10' : 'border-gray-700 text-gray-700'}
`}>
{buffer[i] || ''}
</div>
))}
</div>
{gameState !== 'playing' && (
<div className="animate-fadeUp">
<div className={`text-center p-4 mb-4 border ${gameState === 'won' ? 'border-green-500 bg-green-500/10 text-green-400' : 'border-red-500 bg-red-500/10 text-red-400'}`}>
<div className="text-2xl font-bold mb-1">{gameState === 'won' ? 'ACCESS GRANTED' : 'BREACH FAILED'}</div>
<div className="text-xs uppercase tracking-widest">{gameState === 'won' ? 'System compromised' : 'Trace completed'}</div>
</div>
<button
onClick={() => { playClick(); initGame(); }}
className="w-full py-3 bg-white text-black font-bold uppercase hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
>
<RefreshCw size={16} /> Reboot System
</button>
</div>
)}
{gameState === 'playing' && (
<div className="text-xs text-gray-500 font-mono leading-relaxed">
INSTRUCTIONS:<br/>
1. Match the Target Sequence.<br/>
2. Select codes from the active Row/Column.<br/>
3. The axis switches after each selection.
</div>
)}
</div>
</div>
</div>
</div>
);
};

189
src/pages/Console.tsx Normal file
View File

@@ -0,0 +1,189 @@
import React, { useState, useRef, useEffect } from 'react';
import { useToast } from '../context/ToastContext';
interface HistoryLine {
id: number;
content: React.ReactNode;
type?: 'input' | 'output' | 'error' | 'system';
}
export const Console: React.FC = () => {
const [history, setHistory] = useState<HistoryLine[]>([
{ id: 1, content: 'DIMEDROL OS [Version 4.0.2-alpha]', type: 'system' },
{ id: 2, content: 'Connected to secure node. Type "help" for commands.', type: 'system' },
{ id: 3, content: <br/>, type: 'system' }
]);
const [input, setInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
// Ref для контейнера, который нужно скроллить
const scrollContainerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { addToast } = useToast();
// Эффект: Скроллим вниз ВНУТРИ контейнера при обновлении истории
useEffect(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
}
}, [history]);
// Фокус на инпут при клике в любую часть терминала
const handleContainerClick = () => {
if (!isProcessing) inputRef.current?.focus();
};
const addLine = (content: React.ReactNode, type: 'output' | 'error' | 'system' = 'output') => {
setHistory(prev => [...prev, { id: Date.now() + Math.random(), content, type }]);
};
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const processCommand = async (cmd: string) => {
setIsProcessing(true);
setHistory(prev => [...prev, { id: Date.now(), content: `root@dss:~$ ${cmd}`, type: 'input' }]);
const command = cmd.trim().toLowerCase();
const args = command.split(' ').slice(1); // на будущее для аргументов
const baseCmd = command.split(' ')[0];
switch (baseCmd) {
case 'help':
addLine(
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-gray-400">
<span><strong className="text-indigo-400">help</strong> - Show available commands</span>
<span><strong className="text-indigo-400">clear</strong> - Clear screen buffer</span>
<span><strong className="text-indigo-400">scan</strong> - Network vulnerability scan</span>
<span><strong className="text-indigo-400">whoami</strong> - User identity info</span>
<span><strong className="text-indigo-400">deploy</strong> - Execute active payload</span>
<span><strong className="text-indigo-400">matrix</strong> - Toggle reality filter</span>
</div>
);
break;
case 'clear':
setHistory([]);
break;
case 'whoami':
addLine("uid=0(root) gid=0(root) groups=0(root)");
break;
case 'scan':
addLine("Initializing network scan...", "system");
await delay(800);
addLine(<span className="text-yellow-500">Target detected: 192.168.0.X</span>);
await delay(600);
addLine("Scanning ports [22, 80, 443, 3000]...");
for (let i = 0; i <= 100; i += 25) {
addLine(`[${'#'.repeat(i/10)}${'-'.repeat(10 - i/10)}] ${i}%`, "system");
await delay(300);
}
addLine(<span className="text-green-500">SCAN COMPLETE. 3 Critical Vulnerabilities found.</span>);
addToast("Network scan completed successfully", "success");
break;
case 'deploy':
addLine("Preparing payload injection...", "system");
await delay(1000);
addLine(<span className="text-red-400">ERROR: Firewall detected. Attempting bypass...</span>, "error");
await delay(800);
addLine("Brute-forcing auth token...");
await delay(1500);
addLine(<span className="text-green-500">ACCESS GRANTED. Payload deployed.</span>);
addToast("Payload successfully deployed to target", "success");
break;
case 'matrix':
addLine("The Matrix has you...", "system");
await delay(1000);
addLine("Follow the white rabbit.", "system");
await delay(1000);
addLine("Knock, knock.", "system");
break;
case 'sudo':
addLine(<span className="text-red-500">Nice try. You are already root.</span>, "error");
break;
default:
if (command !== '') {
addLine(`Command not found: ${baseCmd}. Type "help" for usage.`, "error");
}
}
setIsProcessing(false);
// Возвращаем фокус после завершения команды
setTimeout(() => inputRef.current?.focus(), 50);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
processCommand(input);
setInput('');
}
};
return (
<div className="page-enter flex-1 flex flex-col h-full min-h-[70vh] max-h-[80vh]" onClick={handleContainerClick}>
{/* Header терминала */}
<div className="flex items-center justify-between bg-[#1a1a1a] border-t border-x border-white/10 rounded-t-lg p-3 select-none">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/50"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500/50"></div>
<div className="w-3 h-3 rounded-full bg-green-500/50"></div>
</div>
<div className="text-xs text-gray-500 font-mono">root@dss-server:~</div>
<div className="w-10"></div> {/* Spacer */}
</div>
{/* Тело терминала */}
<div className="flex-1 bg-[#0a0a0a]/95 backdrop-blur border border-white/10 rounded-b-lg p-4 font-mono text-sm md:text-base overflow-hidden flex flex-col shadow-2xl relative">
{/* Индикатор обработки */}
{isProcessing && (
<div className="absolute top-2 right-4 text-xs text-indigo-500 animate-pulse font-bold tracking-widest">
PROCESSING...
</div>
)}
{/* Скроллящийся контент */}
<div
ref={scrollContainerRef}
className="flex-1 overflow-y-auto space-y-1 scrollbar-hide pr-2"
>
{history.map((line) => (
<div key={line.id} className={`${
line.type === 'input' ? 'text-white font-bold mt-4 mb-2' :
line.type === 'error' ? 'text-red-400' :
line.type === 'system' ? 'text-indigo-400' : 'text-gray-300'
}`}>
{line.content}
</div>
))}
</div>
{/* Строка ввода (Всегда прижата к низу визуально) */}
<div className={`mt-4 flex gap-2 items-center border-t border-white/10 pt-4 ${isProcessing ? 'opacity-50 grayscale' : ''}`}>
<span className="text-green-500 font-bold shrink-0">root@dss:~$</span>
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isProcessing}
className="bg-transparent border-none outline-none text-white w-full font-mono"
autoFocus
autoComplete="off"
spellCheck="false"
/>
</div>
</div>
</div>
);
};

141
src/pages/Dashboard.tsx Normal file
View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { Activity, Database, Server, Settings, Globe, HardDrive } from 'lucide-react';
import { CyberTabs } from '../components/ui/CyberTabs';
import { TheVault } from '../components/dashboard/TheVault';
import { NeuralMap } from '../components/dashboard/NeuralMap';
// --- Старый код Dashboard (часть Overview) ---
const OverviewTab = () => (
<div className="animate-fadeUp space-y-6">
{/* Статы */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Карточка 1: Indigo */}
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg group hover:border-indigo-500/50 transition-colors">
<div className="text-indigo-500 mb-2"><Server /></div>
<div className="text-2xl font-bold text-white">12/16</div>
<div className="text-xs text-gray-500 uppercase group-hover:text-indigo-400 transition-colors">Active Cores</div>
</div>
{/* Карточка 2: Green */}
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg group hover:border-green-500/50 transition-colors">
<div className="text-green-500 mb-2"><Database /></div>
<div className="text-2xl font-bold text-white">482 TB</div>
<div className="text-xs text-gray-500 uppercase group-hover:text-green-400 transition-colors">Data Processed</div>
</div>
{/* Карточка 3: Yellow (ИСПРАВЛЕНО) */}
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg group hover:border-yellow-500/50 transition-colors">
<div className="text-yellow-500 mb-2"><Activity /></div>
<div className="text-2xl font-bold text-white">99.9%</div>
<div className="text-xs text-gray-500 uppercase group-hover:text-yellow-400 transition-colors">Uptime</div>
</div>
{/* Карточка 4: Red (ИСПРАВЛЕНО) */}
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg group hover:border-red-500/50 transition-colors">
<div className="text-red-500 mb-2 animate-pulse"><Globe /></div>
<div className="text-2xl font-bold text-white">3</div>
<div className="text-xs text-gray-500 uppercase group-hover:text-red-400 transition-colors">Active Alerts</div>
</div>
</div>
{/* Графики и логи (без изменений) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 bg-[#0a0a0a] border border-white/10 p-6 rounded-lg">
<h3 className="text-lg font-bold text-white mb-6 flex items-center gap-2">
<Activity size={18} className="text-indigo-500" /> Real-time Traffic
</h3>
<div className="h-48 flex items-end gap-1 justify-between px-2 border-b border-l border-white/10 relative">
<div className="absolute inset-0 bg-grid opacity-10"></div>
{Array.from({ length: 40 }).map((_, i) => (
<div
key={i}
className="w-full bg-indigo-500/30 hover:bg-indigo-400 transition-all duration-300"
style={{ height: `${Math.random() * 80 + 10}%` }}
></div>
))}
</div>
</div>
<div className="bg-[#0a0a0a] border border-white/10 p-6 rounded-lg">
<h3 className="text-lg font-bold text-white mb-4">System Logs</h3>
<div className="space-y-3 font-mono text-xs">
{[1,2,3,4,5].map((i) => (
<div key={i} className="flex gap-3 text-gray-400 border-b border-white/5 pb-2">
<span className="text-indigo-500">[{new Date().toLocaleTimeString()}]</span>
<span>Packet trace complete. Node {i * 42} verified.</span>
</div>
))}
</div>
</div>
</div>
</div>
);
const SettingsTab = () => (
<div className="max-w-2xl">
<h2 className="text-xl font-bold text-white mb-6">System Preferences</h2>
<div className="space-y-6">
<div className="flex items-center justify-between p-4 border border-white/10 bg-white/5 rounded">
<div>
<div className="text-white font-bold">Stealth Mode</div>
<div className="text-xs text-gray-500">Hide IP address from external scans</div>
</div>
<div className="w-12 h-6 bg-indigo-600 rounded-full relative cursor-pointer">
<div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between p-4 border border-white/10 bg-white/5 rounded">
<div>
<div className="text-white font-bold">Neural Notifications</div>
<div className="text-xs text-gray-500">Direct feed into cerebral cortex</div>
</div>
<div className="w-12 h-6 bg-gray-700 rounded-full relative cursor-pointer">
<div className="absolute left-1 top-1 w-4 h-4 bg-gray-400 rounded-full"></div>
</div>
</div>
<div className="p-4 border border-white/10 bg-white/5 rounded">
<div className="text-white font-bold mb-4">Core Sensitivity</div>
<input type="range" className="w-full h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-indigo-500" />
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>SAFE</span>
<span>AGGRESSIVE</span>
</div>
</div>
</div>
</div>
)
// --- Основной компонент ---
export const Dashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
const tabs = [
{ id: 'overview', label: 'Overview', icon: <Activity size={16} /> },
{ id: 'map', label: 'Network Map', icon: <Globe size={16} /> },
{ id: 'vault', label: 'Data Vault', icon: <HardDrive size={16} /> },
{ id: 'settings', label: 'Config', icon: <Settings size={16} /> },
];
return (
<div className="page-enter">
{/* Header */}
<div className="mb-8 flex items-end justify-between">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Command Center</h1>
<p className="text-gray-400">Authenticated Session: <span className="text-indigo-400">#882-ALPHA</span></p>
</div>
</div>
{/* Навигация */}
<CyberTabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
{/* Контент */}
<div className="min-h-[500px]">
{activeTab === 'overview' && <OverviewTab />}
{activeTab === 'map' && <NeuralMap />}
{activeTab === 'vault' && <TheVault />}
{activeTab === 'settings' && <SettingsTab />}
</div>
</div>
);
};

194
src/pages/Docs.tsx Normal file
View File

@@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { FileText, Server, Box, ChevronRight, Copy, Check, Search, Key } from 'lucide-react';
import { useToast } from '../context/ToastContext';
// 1. СНАЧАЛА ОБЪЯВЛЯЕМ КОМПОНЕНТ CodeBlock
const CodeBlock = ({ code }: { code: string }) => {
const [copied, setCopied] = useState(false);
const { addToast } = useToast();
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
addToast("Code snippet copied to clipboard", "success");
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative group mt-4 mb-6">
<div className="absolute -inset-0.5 bg-gradient-to-r from-indigo-500/20 to-purple-500/20 rounded opacity-50 group-hover:opacity-100 transition duration-500 blur"></div>
<div className="relative bg-[#050505] rounded border border-white/10 p-4 overflow-x-auto">
<button
onClick={handleCopy}
className="absolute top-3 right-3 p-2 bg-white/5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-colors"
>
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
</button>
<pre className="font-mono text-sm text-indigo-300 leading-relaxed">
{code}
</pre>
</div>
</div>
);
};
// 2. ПОТОМ ДАННЫЕ (теперь CodeBlock уже существует)
const DOCS_DATA: any = {
intro: {
title: "Introduction",
icon: <FileText />,
content: (
<div className="space-y-6">
<p className="text-lg text-gray-300 leading-relaxed">
Welcome to <strong className="text-white">DSS (Demonstrus Sub Systems)</strong>.
This is a closed-source, military-grade architectural framework designed for total digital dominance.
Unlike traditional web stacks, DSS operates on a decentralized neural mesh.
</p>
<div className="bg-indigo-500/10 border-l-4 border-indigo-500 p-4 text-sm text-indigo-200">
<strong>NOTE:</strong> Unauthorized usage of this API is a federal offense under the Digital Act of 2077.
</div>
</div>
)
},
installation: {
title: "Installation",
icon: <Box />,
content: (
<div className="space-y-6">
<p>To install the DSS kernel driver, you need high-level privileges.</p>
<CodeBlock code="npm install @dimedrol/dss-kernel --save-dev" />
<CodeBlock code="dss init --force --no-safety" />
<p>Once installed, initialize the neural link:</p>
<CodeBlock code={`import { NeuralLink } from '@dss/core';\n\nconst link = new NeuralLink({\n mode: 'aggressive',\n encryption: 'AES-4096-GCM'\n});\n\nawait link.connect();`} />
</div>
)
},
auth: {
title: "Authentication",
icon: <Key />,
content: (
<div className="space-y-6">
<p>We use a custom JWT (Json Web Token) implementation with quantum-resistant signatures.</p>
<h3 className="text-xl font-bold text-white mt-6">Headers</h3>
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-white/10 text-xs uppercase text-gray-500">
<th className="py-2">Header</th>
<th className="py-2">Type</th>
<th className="py-2">Description</th>
</tr>
</thead>
<tbody className="text-sm font-mono text-gray-300">
<tr className="border-b border-white/5">
<td className="py-2 text-indigo-400">X-DSS-Auth</td>
<td>String</td>
<td>Your API private key</td>
</tr>
<tr className="border-b border-white/5">
<td className="py-2 text-indigo-400">X-Ghost-Mode</td>
<td>Boolean</td>
<td>Enable trace suppression</td>
</tr>
</tbody>
</table>
</div>
)
},
architecture: {
title: "Architecture",
icon: <Server />,
content: (
<div className="space-y-4">
<p>DSS uses a proprietary <span className="text-indigo-400">"Ghost-Node"</span> topology.</p>
<ul className="list-disc pl-5 space-y-2 text-gray-400 marker:text-indigo-500">
<li><strong className="text-white">Layer 1:</strong> The visible web (HTML/Canvas).</li>
<li><strong className="text-white">Layer 2:</strong> The Shadow DOM injection.</li>
<li><strong className="text-white">Layer 3:</strong> Direct WebAssembly kernel access.</li>
</ul>
</div>
)
}
};
// 3. В КОНЦЕ САМ КОМПОНЕНТ СТРАНИЦЫ
export const Docs: React.FC = () => {
const [activeSection, setActiveSection] = useState('intro');
const currentDoc = DOCS_DATA[activeSection];
// Защита от сбоев
if (!currentDoc) return <div>Error loading documentation module.</div>;
return (
<div className="page-enter grid grid-cols-1 lg:grid-cols-4 gap-8 mt-8 min-h-[80vh]">
{/* SIDEBAR */}
<aside className="lg:col-span-1">
<div className="sticky top-24 space-y-8">
{/* Search Mock */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={16} />
<input
type="text"
placeholder="Search documentation..."
className="w-full bg-white/5 border border-white/10 rounded py-2 pl-10 pr-4 text-sm text-white focus:border-indigo-500 focus:outline-none transition-colors"
/>
</div>
{/* Navigation */}
<div className="space-y-1">
<p className="px-3 text-xs font-bold text-gray-500 uppercase tracking-widest mb-2">Core Modules</p>
{Object.keys(DOCS_DATA).map((key) => {
const item = DOCS_DATA[key];
const isActive = activeSection === key;
return (
<button
key={key}
onClick={() => setActiveSection(key)}
className={`w-full flex items-center gap-3 px-3 py-2 text-sm rounded transition-all ${
isActive
? 'bg-indigo-500/10 text-indigo-400 border-l-2 border-indigo-500'
: 'text-gray-400 hover:text-white hover:bg-white/5 border-l-2 border-transparent'
}`}
>
{React.cloneElement(item.icon, { size: 16 })}
{item.title}
{isActive && <ChevronRight size={14} className="ml-auto" />}
</button>
);
})}
</div>
{/* Fake API Status */}
<div className="p-4 bg-[#0a0a0a] border border-white/10 rounded">
<div className="text-xs text-gray-500 mb-2">API SERVER STATUS</div>
<div className="flex items-center gap-2 text-green-500 font-bold text-sm">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
OPERATIONAL
</div>
</div>
</div>
</aside>
{/* MAIN CONTENT */}
<main className="lg:col-span-3">
<div className="bg-[#0a0a0a]/50 border border-white/5 rounded-2xl p-8 md:p-12 relative overflow-hidden">
<div className="absolute top-0 right-0 -translate-y-1/2 translate-x-1/3 w-96 h-96 bg-indigo-500/10 blur-[100px] rounded-full pointer-events-none"></div>
<div className="relative z-10 animate-fadeUp key={activeSection}">
<div className="flex items-center gap-4 mb-8 border-b border-white/10 pb-6">
<div className="p-3 bg-indigo-500/20 rounded text-indigo-400">
{React.cloneElement(currentDoc.icon, { size: 32 })}
</div>
<h1 className="text-4xl font-bold text-white">{currentDoc.title}</h1>
</div>
<div className="prose prose-invert prose-indigo max-w-none">
{currentDoc.content}
</div>
</div>
</div>
</main>
</div>
);
};

75
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ChevronRight, Activity, Cpu, ShieldAlert, Zap } from 'lucide-react';
import { CyberButton } from '../components/ui/CyberButton';
import { StatItem } from '../components/ui/StatItem';
import { FeatureCard } from '../components/ui/FeatureCard';
import { useTypewriter } from '../hooks/useTypewriter';
export const Home: React.FC = () => {
const text = useTypewriter("INITIALIZING PROTOCOL...", 70);
return (
<div className="page-enter space-y-32">
<section className="flex flex-col items-start justify-center min-h-[60vh] border-l border-white/10 pl-8 md:pl-16 relative mt-10">
<div className="absolute left-0 top-0 h-full w-[1px] bg-gradient-to-b from-transparent via-indigo-500 to-transparent" />
<div className="inline-flex items-center gap-2 px-3 py-1 rounded border border-indigo-500/30 bg-indigo-500/10 text-indigo-400 text-xs mb-6">
<Activity size={14} />
SYSTEM UPDATE 4.2 AVAILABLE
</div>
<p className="text-indigo-400 text-sm md:text-base mb-4 font-bold">
<span className="mr-2 text-white">root@dss:~#</span>
{text}<span className="cursor-blink">_</span>
</p>
<h1 className="text-5xl md:text-8xl font-black tracking-tighter leading-[0.9] mb-6 text-white">
REWRITE <br/>
<span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 via-purple-400 to-indigo-400">
REALITY
</span>
</h1>
<p className="max-w-2xl text-gray-400 text-lg leading-relaxed mb-10 border-l-2 border-indigo-500/50 pl-6">
Advanced software architecture for the digital elite.
We don't build websites. We build digital dominance.
</p>
<div className="flex flex-col md:flex-row gap-4 w-full md:w-auto">
<Link to="/console">
<CyberButton variant="primary">Initialize Console <ChevronRight size={16} /></CyberButton>
</Link>
<Link to="/docs">
<CyberButton variant="secondary">Read The Docs</CyberButton>
</Link>
</div>
</section>
<section className="grid grid-cols-2 md:grid-cols-4 gap-px bg-white/10 border border-white/10">
<StatItem label="Uptime" value="99.99%" />
<StatItem label="Modules" value="142" />
<StatItem label="Encrypted" value="100%" />
<StatItem label="Ping" value="12ms" />
</section>
<section className="grid grid-cols-1 md:grid-cols-3 gap-6">
<FeatureCard
icon={<Cpu size={32} />}
title="Core Optimization"
desc="DSS rewrites the kernel logic to bypass standard limitations. Your system will fly."
/>
<FeatureCard
icon={<ShieldAlert size={32} />}
title="Ghost Protocol"
desc="Complete anonymity. Our encryption layers are theoretically unbreakable."
/>
<FeatureCard
icon={<Zap size={32} />}
title="Neural Sync"
desc="AI-driven predictive algorithms that react before you even click."
/>
</section>
</div>
);
};

142
src/pages/Marketplace.tsx Normal file
View File

@@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { ShoppingCart, Shield, Zap, Cpu, Lock, Star, Package } from 'lucide-react';
import { CyberModal } from '../components/ui/CyberModal';
import { useToast } from '../context/ToastContext';
// Типы товаров
interface Item {
id: number;
name: string;
type: string;
price: number;
rarity: 'common' | 'rare' | 'legendary';
icon: React.ReactNode;
desc: string;
}
const MARKET_ITEMS: Item[] = [
{ id: 1, name: 'Ghost VPN', type: 'Software', price: 500, rarity: 'common', icon: <Shield />, desc: 'Basic IP masking protocol. Standard issue.' },
{ id: 2, name: 'Neural Spike v1', type: 'Exploit', price: 1200, rarity: 'rare', icon: <Zap />, desc: 'Overloads target synapses. 60% success rate.' },
{ id: 3, name: 'Quantum Core', type: 'Hardware', price: 5000, rarity: 'legendary', icon: <Cpu />, desc: 'Unlocks parallel processing threads.' },
{ id: 4, name: 'Brute Force Script', type: 'Script', price: 300, rarity: 'common', icon: <Lock />, desc: 'Simple dictionary attack algorithm.' },
{ id: 5, name: 'AI Companion', type: 'Module', price: 8500, rarity: 'legendary', icon: <Star />, desc: 'Autonomous helper node. Sentience not guaranteed.' },
{ id: 6, name: 'Data Miner', type: 'Utility', price: 2000, rarity: 'rare', icon: <Package />, desc: 'Passively collects data fragments from open ports.' },
];
export const Marketplace: React.FC = () => {
const [credits, setCredits] = useState(2500); // Стартовый баланс
const [ownedItems, setOwnedItems] = useState<number[]>([]);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
const { addToast } = useToast();
const handlePurchaseClick = (item: Item) => {
if (ownedItems.includes(item.id)) return;
setSelectedItem(item);
};
const confirmPurchase = () => {
if (!selectedItem) return;
if (credits >= selectedItem.price) {
setCredits(prev => prev - selectedItem.price);
setOwnedItems(prev => [...prev, selectedItem.id]);
addToast(`Purchased: ${selectedItem.name}`, 'success');
setSelectedItem(null);
} else {
addToast('TRANSACTION FAILED: Insufficient funds.', 'error');
setSelectedItem(null);
}
};
return (
<div className="page-enter min-h-screen">
{/* Header Рынка */}
<div className="mb-10 flex items-end justify-between border-b border-white/10 pb-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Black Market</h1>
<p className="text-gray-400">Acquire illicit tools and hardware upgrades.</p>
</div>
<div className="flex items-center gap-3 px-4 py-2 bg-indigo-500/10 border border-indigo-500/30 rounded">
<span className="text-xs text-indigo-300 uppercase tracking-widest">Balance</span>
<span className="text-xl font-bold text-white font-mono">{credits} CR</span>
</div>
</div>
{/* Сетка товаров */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{MARKET_ITEMS.map((item) => {
const isOwned = ownedItems.includes(item.id);
const canAfford = credits >= item.price;
return (
<div
key={item.id}
className={`group relative bg-[#0a0a0a] border transition-all duration-300 overflow-hidden flex flex-col
${isOwned ? 'border-green-500/30 opacity-50' : 'border-white/10 hover:border-indigo-500/50 hover:-translate-y-1'}
`}
>
{/* Rarity Stripe */}
<div className={`absolute top-0 left-0 w-1 h-full
${item.rarity === 'legendary' ? 'bg-yellow-500' : item.rarity === 'rare' ? 'bg-purple-500' : 'bg-gray-500'}
`}></div>
<div className="p-6 flex-1">
<div className="flex justify-between items-start mb-4 pl-2">
<div className={`p-3 rounded bg-white/5 ${item.rarity === 'legendary' ? 'text-yellow-500' : item.rarity === 'rare' ? 'text-purple-400' : 'text-gray-400'}`}>
{item.icon}
</div>
<span className="text-[10px] uppercase tracking-widest border border-white/10 px-2 py-1 text-gray-500 rounded">
{item.type}
</span>
</div>
<h3 className="text-xl font-bold text-white mb-2 pl-2">{item.name}</h3>
<p className="text-sm text-gray-400 pl-2 leading-relaxed mb-6">{item.desc}</p>
</div>
<div className="p-4 border-t border-white/10 bg-white/5 pl-6 flex items-center justify-between">
<span className="font-mono text-lg font-bold text-white">{item.price} CR</span>
<button
onClick={() => handlePurchaseClick(item)}
disabled={isOwned}
className={`px-4 py-2 text-xs font-bold uppercase tracking-widest transition-colors flex items-center gap-2
${isOwned
? 'text-green-500 cursor-default'
: canAfford
? 'bg-white text-black hover:bg-indigo-400 hover:text-white'
: 'text-gray-600 cursor-not-allowed'}
`}
>
{isOwned ? (
<>Owned <ShoppingCart size={14} /></>
) : (
<>Buy <ShoppingCart size={14} /></>
)}
</button>
</div>
</div>
);
})}
</div>
{/* Модалка подтверждения */}
<CyberModal
isOpen={!!selectedItem}
onClose={() => setSelectedItem(null)}
onConfirm={confirmPurchase}
title="Confirm Transaction"
type="info"
>
<p>Are you sure you want to purchase <strong className="text-white">{selectedItem?.name}</strong>?</p>
<div className="mt-4 p-4 bg-white/5 border border-white/10 rounded flex justify-between items-center">
<span className="text-sm text-gray-400">Total Cost:</span>
<span className="text-xl font-bold text-indigo-400 font-mono">{selectedItem?.price} CR</span>
</div>
<p className="mt-4 text-xs text-gray-500">
WARNING: Digital goods are non-refundable. Ensure your neural rig is compatible before purchase.
</p>
</CyberModal>
</div>
);
};

169
src/pages/Operations.tsx Normal file
View File

@@ -0,0 +1,169 @@
import React, { useState } from 'react';
import { Crosshair, Clock, AlertTriangle, Trophy, Play, Square } from 'lucide-react';
import { useToast } from '../context/ToastContext';
import { LiveLog } from '../components/dashboard/LiveLog';
import { StatItem } from '../components/ui/StatItem';
interface Mission {
id: number;
title: string;
target: string;
difficulty: 'Low' | 'Medium' | 'High' | 'Suicide';
reward: number;
duration: number; // seconds
risk: number; // % failure chance
}
const MISSIONS: Mission[] = [
{ id: 1, title: "Data Scrape", target: "Local ISP", difficulty: "Low", reward: 150, duration: 3, risk: 10 },
{ id: 2, title: "Proxy Override", target: "Gov Server", difficulty: "Medium", reward: 450, duration: 5, risk: 30 },
{ id: 3, title: "Neural Heist", target: "Arasaka Corp", difficulty: "High", reward: 1200, duration: 8, risk: 60 },
{ id: 4, title: "AI Liberation", target: "The Core", difficulty: "Suicide", reward: 5000, duration: 12, risk: 85 },
];
export const Operations: React.FC = () => {
const [activeMission, setActiveMission] = useState<number | null>(null);
const [progress, setProgress] = useState(0);
// Здесь можно было бы подключить контекст кредитов, но для демо покажем локально
// В идеале нужно вынести Credits в отдельный Context, как Auth
const [localCredits, setLocalCredits] = useState(2500);
const { addToast } = useToast();
const startMission = (mission: Mission) => {
if (activeMission) return;
setActiveMission(mission.id);
setProgress(0);
addToast(`Mission started: ${mission.title}`, "info");
let currentProgress = 0;
const intervalTime = (mission.duration * 1000) / 100;
const timer = setInterval(() => {
currentProgress += 1;
setProgress(currentProgress);
if (currentProgress >= 100) {
clearInterval(timer);
finishMission(mission);
}
}, intervalTime);
};
const finishMission = (mission: Mission) => {
setActiveMission(null);
setProgress(0);
// Расчет успеха
const roll = Math.random() * 100;
if (roll > mission.risk) {
// Успех
setLocalCredits(prev => prev + mission.reward);
addToast(`MISSION SUCCESS. Reward: ${mission.reward} CR transferred.`, "success");
} else {
// Провал
addToast(`MISSION FAILED. Connection traced. Abort!`, "error");
}
};
return (
<div className="page-enter min-h-screen">
{/* Header */}
<div className="mb-8 border-b border-white/10 pb-6 flex flex-col md:flex-row justify-between items-end gap-4">
<div>
<h1 className="text-3xl font-bold text-white mb-2 flex items-center gap-2">
<Crosshair className="text-red-500" /> Operations
</h1>
<p className="text-gray-400">Select a contract. Execute the payload. Get paid.</p>
</div>
{/* Balance Display (Local demo) */}
<div className="bg-[#0a0a0a] border border-white/10 px-6 py-3 rounded flex flex-col items-end">
<span className="text-xs text-gray-500 uppercase tracking-widest">Current Balance</span>
<span className="text-2xl font-bold text-white font-mono">{localCredits} CR</span>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Mission List */}
<div className="lg:col-span-2 space-y-4">
{MISSIONS.map((mission) => {
const isActive = activeMission === mission.id;
const isBusy = activeMission !== null && !isActive;
let difficultyColor = 'text-gray-400';
if (mission.difficulty === 'Medium') difficultyColor = 'text-yellow-500';
if (mission.difficulty === 'High') difficultyColor = 'text-orange-500';
if (mission.difficulty === 'Suicide') difficultyColor = 'text-red-600 animate-pulse';
return (
<div
key={mission.id}
className={`relative bg-[#0a0a0a] border rounded-lg overflow-hidden transition-all p-6
${isActive ? 'border-indigo-500 ring-1 ring-indigo-500/50' : 'border-white/10 hover:border-white/30'}
${isBusy ? 'opacity-50 grayscale' : ''}
`}
>
{/* Progress Bar Background */}
{isActive && (
<div className="absolute bottom-0 left-0 h-1 bg-indigo-500/20 w-full">
<div className="h-full bg-indigo-500 transition-all duration-200" style={{ width: `${progress}%` }}></div>
</div>
)}
<div className="flex flex-col md:flex-row justify-between items-center gap-4 relative z-10">
{/* Info */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold text-white">{mission.title}</h3>
<span className={`text-xs border border-white/10 px-2 py-0.5 rounded uppercase bg-white/5 ${difficultyColor}`}>
{mission.difficulty}
</span>
</div>
<div className="text-sm text-gray-400 flex gap-4 font-mono">
<span className="flex items-center gap-1"><Crosshair size={12}/> {mission.target}</span>
<span className="flex items-center gap-1"><Clock size={12}/> {mission.duration}s</span>
<span className="flex items-center gap-1 text-red-400"><AlertTriangle size={12}/> {mission.risk}% Risk</span>
</div>
</div>
{/* Rewards & Action */}
<div className="flex items-center gap-6">
<div className="text-right">
<div className="text-xs text-gray-500 uppercase">Reward</div>
<div className="text-xl font-bold text-green-400 font-mono">{mission.reward} CR</div>
</div>
<button
onClick={() => startMission(mission)}
disabled={activeMission !== null}
className={`
w-12 h-12 flex items-center justify-center rounded border transition-all
${isActive
? 'bg-indigo-500 border-indigo-400 text-white'
: 'bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/30 text-gray-400 hover:text-white'}
${isBusy ? 'cursor-not-allowed' : ''}
`}
>
{isActive ? <Square size={20} className="animate-pulse" /> : <Play size={20} />}
</button>
</div>
</div>
</div>
);
})}
</div>
{/* Sidebar: Live Feed */}
<div className="h-[600px] sticky top-24">
<LiveLog />
</div>
</div>
</div>
);
};

28
tsconfig.app.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

10
vite.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})