Initial commit: Cyberpunk Dashboard
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
12
Dockerfile
Normal 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
73
README.md
Normal 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
11
docker-compose.yml
Normal 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
23
eslint.config.js
Normal 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
13
index.html
Normal 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
20
nginx.conf
Normal 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
4056
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
1
public/vite.svg
Normal 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
42
src/App.css
Normal 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
53
src/App.tsx
Normal 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
1
src/assets/react.svg
Normal 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 |
74
src/components/dashboard/LiveLog.tsx
Normal file
74
src/components/dashboard/LiveLog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
55
src/components/dashboard/NeuralMap.tsx
Normal file
55
src/components/dashboard/NeuralMap.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
128
src/components/dashboard/TheVault.tsx
Normal file
128
src/components/dashboard/TheVault.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
9
src/components/layout/BackgroundEffects.tsx
Normal file
9
src/components/layout/BackgroundEffects.tsx
Normal 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" />
|
||||
</>
|
||||
);
|
||||
26
src/components/layout/Layout.tsx
Normal file
26
src/components/layout/Layout.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
99
src/components/layout/Navbar.tsx
Normal file
99
src/components/layout/Navbar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
15
src/components/layout/ProtectedRoute.tsx
Normal file
15
src/components/layout/ProtectedRoute.tsx
Normal 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}</>;
|
||||
};
|
||||
43
src/components/ui/CyberButton.tsx
Normal file
43
src/components/ui/CyberButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
37
src/components/ui/CyberInput.tsx
Normal file
37
src/components/ui/CyberInput.tsx
Normal 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>
|
||||
|
||||
{/* ИСПРАВЛЕНИЕ ЗДЕСЬ: заменили > на > */}
|
||||
{error && (
|
||||
<p className="text-red-500 text-xs mt-1 animate-pulse">
|
||||
> ERROR: {error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
95
src/components/ui/CyberModal.tsx
Normal file
95
src/components/ui/CyberModal.tsx
Normal 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)
|
||||
);
|
||||
};
|
||||
41
src/components/ui/CyberTabs.tsx
Normal file
41
src/components/ui/CyberTabs.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
src/components/ui/FeatureCard.tsx
Normal file
24
src/components/ui/FeatureCard.tsx
Normal 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>
|
||||
);
|
||||
17
src/components/ui/StatItem.tsx
Normal file
17
src/components/ui/StatItem.tsx
Normal 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>
|
||||
);
|
||||
12
src/components/utils/ScrollToTop.tsx
Normal file
12
src/components/utils/ScrollToTop.tsx
Normal 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;
|
||||
};
|
||||
90
src/context/AuthContext.tsx
Normal file
90
src/context/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
98
src/context/SoundContext.tsx
Normal file
98
src/context/SoundContext.tsx
Normal 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;
|
||||
};
|
||||
75
src/context/ToastContext.tsx
Normal file
75
src/context/ToastContext.tsx
Normal 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;
|
||||
};
|
||||
17
src/hooks/useTypewriter.ts
Normal file
17
src/hooks/useTypewriter.ts
Normal 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
85
src/index.css
Normal 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
10
src/main.tsx
Normal 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
130
src/pages/AuthPage.tsx
Normal 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
197
src/pages/Breach.tsx
Normal 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
189
src/pages/Console.tsx
Normal 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
141
src/pages/Dashboard.tsx
Normal 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
194
src/pages/Docs.tsx
Normal 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
75
src/pages/Home.tsx
Normal 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
142
src/pages/Marketplace.tsx
Normal 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
169
src/pages/Operations.tsx
Normal 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
28
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal 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
10
vite.config.ts
Normal 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(),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user