first commit
This commit is contained in:
46
bot/frontend/package.json
Normal file
46
bot/frontend/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "skrzynka-impostora-panel",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"axios": "^1.3.4",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"lucide-react": "^0.321.0",
|
||||
"clsx": "^1.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:3000"
|
||||
}
|
||||
17
bot/frontend/public/index.html
Normal file
17
bot/frontend/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#5865f2" />
|
||||
<meta name="description" content="Panel zarządzania bota Discord - Skrzynka Impostora" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Skrzynka Impostora - Panel Administracyjny</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Musisz włączyć JavaScript aby korzystać z tej aplikacji.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
bot/frontend/public/manifest.json
Normal file
15
bot/frontend/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Skrzynka Impostora",
|
||||
"name": "Skrzynka Impostora Bot Panel",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#5865f2",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
708
bot/frontend/src/App.css
Normal file
708
bot/frontend/src/App.css
Normal file
@@ -0,0 +1,708 @@
|
||||
/* Discord-inspired CSS for Skrzynka Impostora Bot Panel */
|
||||
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
--discord-blue: #5865f2;
|
||||
--discord-green: #57f287;
|
||||
--discord-red: #ed4245;
|
||||
--discord-yellow: #fee75c;
|
||||
--discord-purple: #eb459e;
|
||||
|
||||
--discord-dark: #2c2f33;
|
||||
--discord-darker: #23272a;
|
||||
--discord-light: #36393f;
|
||||
--discord-lighter: #40444b;
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b9bbbe;
|
||||
--text-muted: #72767d;
|
||||
|
||||
--background-primary: #36393f;
|
||||
--background-secondary: #2f3136;
|
||||
--background-tertiary: #202225;
|
||||
|
||||
--border-color: #202225;
|
||||
--hover-color: rgba(79, 84, 92, 0.16);
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.sidebar p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: var(--hover-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--discord-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: 260px;
|
||||
padding: 2rem;
|
||||
background-color: var(--background-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.discord-card {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.guild-card {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.guild-card:hover {
|
||||
border-color: var(--discord-blue);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Guild icon */
|
||||
.guild-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--discord-blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-right: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.discord-button {
|
||||
background-color: var(--discord-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.discord-button:hover {
|
||||
background-color: #4752c4;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.discord-button:disabled {
|
||||
background-color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.discord-button.secondary {
|
||||
background-color: var(--background-lighter);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.discord-button.secondary:hover {
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
.discord-button.danger {
|
||||
background-color: var(--discord-red);
|
||||
}
|
||||
|
||||
.discord-button.danger:hover {
|
||||
background-color: #c73e41;
|
||||
}
|
||||
|
||||
.discord-button.success {
|
||||
background-color: var(--discord-green);
|
||||
}
|
||||
|
||||
.discord-button.success:hover {
|
||||
background-color: #4ac486;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.discord-textarea {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
background-color: var(--background-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.discord-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--discord-blue);
|
||||
}
|
||||
|
||||
.discord-textarea::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Preview area */
|
||||
.preview-area {
|
||||
background-color: var(--background-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-area h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview-area h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin: 0 0 0.75rem 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview-area h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: bold;
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview-area p {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.preview-area strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.preview-area em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.preview-area code {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--discord-yellow);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.preview-area pre {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.preview-area pre code {
|
||||
background: none;
|
||||
color: var(--discord-green);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Grid system */
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Spacing */
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-8 { margin-bottom: 2rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
.mt-8 { margin-top: 2rem; }
|
||||
.mr-2 { margin-right: 0.5rem; }
|
||||
.mr-3 { margin-right: 0.75rem; }
|
||||
.mr-4 { margin-right: 1rem; }
|
||||
.ml-4 { margin-left: 1rem; }
|
||||
|
||||
/* Text utilities */
|
||||
.text-center { text-align: center; }
|
||||
.text-sm { font-size: 0.875rem; }
|
||||
.text-lg { font-size: 1.125rem; }
|
||||
.text-xl { font-size: 1.25rem; }
|
||||
.text-2xl { font-size: 1.5rem; }
|
||||
.text-3xl { font-size: 1.875rem; }
|
||||
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
|
||||
.text-gray-900 { color: var(--text-primary); }
|
||||
.text-gray-600 { color: var(--text-secondary); }
|
||||
.text-gray-500 { color: var(--text-muted); }
|
||||
|
||||
/* Status indicators */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgba(237, 66, 69, 0.1);
|
||||
border: 1px solid var(--discord-red);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
color: var(--discord-red);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: rgba(87, 242, 135, 0.1);
|
||||
border: 1px solid var(--discord-green);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
color: var(--discord-green);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.grid-cols-2,
|
||||
.grid-cols-3,
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.discord-card,
|
||||
.guild-card {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--text-muted);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Discord-like styling */
|
||||
.discord-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.discord-button {
|
||||
background: #5865f2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 10px 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.discord-button:hover {
|
||||
background: #4752c4;
|
||||
}
|
||||
|
||||
.discord-button:disabled {
|
||||
background: #a5a5a5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.discord-button.secondary {
|
||||
background: #4f545c;
|
||||
}
|
||||
|
||||
.discord-button.secondary:hover {
|
||||
background: #5d6269;
|
||||
}
|
||||
|
||||
.discord-button.danger {
|
||||
background: #ed4245;
|
||||
}
|
||||
|
||||
.discord-button.danger:hover {
|
||||
background: #c03537;
|
||||
}
|
||||
|
||||
.discord-input {
|
||||
background: #f2f3f5;
|
||||
border: 1px solid #e3e5e8;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.discord-input:focus {
|
||||
outline: none;
|
||||
border-color: #5865f2;
|
||||
}
|
||||
|
||||
.discord-textarea {
|
||||
background: #f2f3f5;
|
||||
border: 1px solid #e3e5e8;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.discord-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #5865f2;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background: #2f3136;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #b9bbbe;
|
||||
text-decoration: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: #393c43;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: #5865f2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
background: #36393f;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
min-height: 200px;
|
||||
font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.preview-area h1, .preview-area h2, .preview-area h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
color: #0369a1;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.guild-card {
|
||||
background: white;
|
||||
border: 1px solid #e3e5e8;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.guild-card:hover {
|
||||
border-color: #5865f2;
|
||||
box-shadow: 0 2px 8px rgba(88, 101, 242, 0.1);
|
||||
}
|
||||
|
||||
.guild-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #5865f2;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
31
bot/frontend/src/App.js
Normal file
31
bot/frontend/src/App.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider } from './hooks/useAuth';
|
||||
import Layout from './components/Layout';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import MessageEditor from './pages/MessageEditor';
|
||||
import ServerSelect from './pages/ServerSelect';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="servers" element={<ServerSelect />} />
|
||||
<Route path="servers/:guildId/messages" element={<MessageEditor />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
70
bot/frontend/src/components/Layout.js
Normal file
70
bot/frontend/src/components/Layout.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { LogOut, Home, Server, Settings } from 'lucide-react';
|
||||
|
||||
function Layout() {
|
||||
const { user, logout, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<aside className="sidebar">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-bold text-white mb-2">🎭 Skrzynka Impostora</h2>
|
||||
<p className="text-gray-300 text-sm">Panel administratora</p>
|
||||
</div>
|
||||
|
||||
<nav className="mb-8">
|
||||
<NavLink to="/dashboard" className="nav-link">
|
||||
<Home size={16} className="inline mr-2" />
|
||||
Dashboard
|
||||
</NavLink>
|
||||
<NavLink to="/servers" className="nav-link">
|
||||
<Server size={16} className="inline mr-2" />
|
||||
Serwery
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto">
|
||||
<div className="border-t border-gray-600 pt-4">
|
||||
<div className="text-sm text-gray-300 mb-2">
|
||||
Zalogowany jako:
|
||||
</div>
|
||||
<div className="text-white font-medium mb-4">
|
||||
{user?.username}#{user?.discriminator}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="discord-button secondary w-full flex items-center justify-center"
|
||||
>
|
||||
<LogOut size={16} className="mr-2" />
|
||||
Wyloguj
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="main-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
67
bot/frontend/src/hooks/useAuth.js
Normal file
67
bot/frontend/src/hooks/useAuth.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { api } from '../services/api';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [token, setToken] = useState(localStorage.getItem('auth_token'));
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
api.setAuthToken(token);
|
||||
verifyToken();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const verifyToken = async () => {
|
||||
try {
|
||||
const response = await api.verifyToken();
|
||||
setUser(response.data.user);
|
||||
} catch (error) {
|
||||
console.error('Token verification failed:', error);
|
||||
logout();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const login = (authToken) => {
|
||||
setToken(authToken);
|
||||
localStorage.setItem('auth_token', authToken);
|
||||
api.setAuthToken(authToken);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
localStorage.removeItem('auth_token');
|
||||
api.setAuthToken(null);
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
login,
|
||||
logout,
|
||||
isAuthenticated: !!user
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
13
bot/frontend/src/index.css
Normal file
13
bot/frontend/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
11
bot/frontend/src/index.js
Normal file
11
bot/frontend/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
213
bot/frontend/src/pages/Dashboard.js
Normal file
213
bot/frontend/src/pages/Dashboard.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '../services/api';
|
||||
import { Server, MessageSquare, Users, Activity } from 'lucide-react';
|
||||
|
||||
function Dashboard() {
|
||||
const [stats, setStats] = useState({
|
||||
totalServers: 0,
|
||||
totalMessages: 0,
|
||||
totalUsers: 0,
|
||||
botStatus: 'online'
|
||||
});
|
||||
const [recentActivity, setRecentActivity] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
// Mock data - w prawdziwej aplikacji pobierz z API
|
||||
setStats({
|
||||
totalServers: 5,
|
||||
totalMessages: 23,
|
||||
totalUsers: 1,
|
||||
botStatus: 'online'
|
||||
});
|
||||
|
||||
setRecentActivity([
|
||||
{
|
||||
id: 1,
|
||||
type: 'message_updated',
|
||||
server: 'Test Server',
|
||||
timestamp: new Date(),
|
||||
description: 'Zaktualizowano wiadomość powitalną'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'channel_configured',
|
||||
server: 'Test Server',
|
||||
timestamp: new Date(Date.now() - 3600000),
|
||||
description: 'Skonfigurowano kanał #witamy'
|
||||
}
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Ładowanie dashboard...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Dashboard</h1>
|
||||
<p className="text-gray-600">
|
||||
Przegląd stanu bota i ostatnich aktywności
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="discord-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-blue-100 rounded-lg mr-4">
|
||||
<Server className="text-blue-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Serwery</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalServers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="discord-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-green-100 rounded-lg mr-4">
|
||||
<MessageSquare className="text-green-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Wiadomości</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalMessages}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="discord-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-purple-100 rounded-lg mr-4">
|
||||
<Users className="text-purple-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Użytkownicy</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalUsers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="discord-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-yellow-100 rounded-lg mr-4">
|
||||
<Activity className="text-yellow-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Status Bota</p>
|
||||
<div className="flex items-center">
|
||||
<div className={`w-3 h-3 rounded-full mr-2 ${
|
||||
stats.botStatus === 'online' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<p className="text-lg font-semibold text-gray-900 capitalize">
|
||||
{stats.botStatus === 'online' ? 'Online' : 'Offline'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Quick Actions */}
|
||||
<div className="discord-card">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Szybkie akcje</h3>
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
to="/servers"
|
||||
className="block p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Server className="text-blue-600 mr-3" size={20} />
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Zarządzaj serwerami</p>
|
||||
<p className="text-sm text-gray-600">Konfiguruj kanały i wiadomości</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="block p-3 border border-gray-200 rounded-lg bg-gray-50">
|
||||
<div className="flex items-center">
|
||||
<MessageSquare className="text-gray-400 mr-3" size={20} />
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Szablony wiadomości</p>
|
||||
<p className="text-sm text-gray-400">Wkrótce dostępne</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="block p-3 border border-gray-200 rounded-lg bg-gray-50">
|
||||
<div className="flex items-center">
|
||||
<Activity className="text-gray-400 mr-3" size={20} />
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Statystyki</p>
|
||||
<p className="text-sm text-gray-400">Wkrótce dostępne</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="discord-card">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ostatnia aktywność</h3>
|
||||
<div className="space-y-3">
|
||||
{recentActivity.length > 0 ? (
|
||||
recentActivity.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start p-3 bg-gray-50 rounded-lg">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3"></div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{activity.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{activity.server} • {activity.timestamp.toLocaleString('pl-PL')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 text-center py-4">
|
||||
Brak ostatnich aktywności
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bot Info */}
|
||||
<div className="discord-card mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Informacje o bocie</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Wersja</p>
|
||||
<p className="font-medium">1.0.0</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Czas działania</p>
|
||||
<p className="font-medium">99.5%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Ostatnia aktualizacja</p>
|
||||
<p className="font-medium">Dzisiaj</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
82
bot/frontend/src/pages/Login.js
Normal file
82
bot/frontend/src/pages/Login.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
function Login() {
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// Sprawdź czy w URL jest token (po przekierowaniu z OAuth)
|
||||
const token = searchParams.get('token');
|
||||
if (token) {
|
||||
login(token);
|
||||
navigate('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
// Jeśli już zalogowany, przekieruj na dashboard
|
||||
if (isAuthenticated) {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}, [searchParams, login, navigate, isAuthenticated]);
|
||||
|
||||
const handleDiscordLogin = () => {
|
||||
const apiUrl = process.env.NODE_ENV === 'production'
|
||||
? '/api/auth/discord'
|
||||
: 'http://localhost:3000/api/auth/discord';
|
||||
|
||||
window.location.href = apiUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4">🎭</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900">
|
||||
Skrzynka Impostora Bot
|
||||
</h2>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Panel zarządzania wiadomościami powaitalnymi
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="discord-card">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Zaloguj się za pomocą Discord
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
Aby zarządzać wiadomościami powaitalnymi, musisz się zalogować przez Discord.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleDiscordLogin}
|
||||
className="discord-button w-full flex items-center justify-center text-lg py-3"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 mr-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.010c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Zaloguj przez Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-gray-500">
|
||||
<p>
|
||||
Potrzebujesz uprawnień administratora na serwerze Discord,
|
||||
aby zarządzać wiadomościami powaitalnymi.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
303
bot/frontend/src/pages/MessageEditor.js
Normal file
303
bot/frontend/src/pages/MessageEditor.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../services/api';
|
||||
import { Save, Eye, History, ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
function MessageEditor() {
|
||||
const { guildId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [message, setMessage] = useState({
|
||||
content: '',
|
||||
embed_data: null
|
||||
});
|
||||
const [originalMessage, setOriginalMessage] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(null);
|
||||
const [previewMode, setPreviewMode] = useState('discord');
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadMessage();
|
||||
}, [guildId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (originalMessage) {
|
||||
setIsDirty(message.content !== originalMessage.content);
|
||||
}
|
||||
}, [message.content, originalMessage]);
|
||||
|
||||
const loadMessage = async () => {
|
||||
try {
|
||||
const response = await api.getMessage(guildId);
|
||||
const loadedMessage = response.data;
|
||||
|
||||
setMessage(loadedMessage);
|
||||
setOriginalMessage(loadedMessage);
|
||||
} catch (error) {
|
||||
console.error('Error loading message:', error);
|
||||
setError('Nie udało się załadować wiadomości');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveMessage = async () => {
|
||||
if (!message.content.trim()) {
|
||||
setError('Treść wiadomości nie może być pusta');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.content.length > 2000) {
|
||||
setError('Treść wiadomości nie może przekraczać 2000 znaków');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await api.saveMessage(guildId, message.content, message.embed_data);
|
||||
setSuccess('Wiadomość została zapisana pomyślnie');
|
||||
setOriginalMessage({ ...message });
|
||||
setIsDirty(false);
|
||||
|
||||
// Ukryj komunikat po 3 sekundach
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error saving message:', error);
|
||||
setError('Nie udało się zapisać wiadomości');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetMessage = () => {
|
||||
if (originalMessage) {
|
||||
setMessage({ ...originalMessage });
|
||||
setIsDirty(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDiscordPreview = (content) => {
|
||||
// Podstawowa konwersja Discord Markdown do HTML
|
||||
let html = content
|
||||
// Headers
|
||||
.replace(/^# (.*$)/gim, '<h1 class="text-xl font-bold mb-2">$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2 class="text-lg font-bold mb-2">$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3 class="text-base font-bold mb-1">$1</h3>')
|
||||
// Bold
|
||||
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
|
||||
// Underline
|
||||
.replace(/__(.*?)__/gim, '<u>$1</u>')
|
||||
// Strikethrough
|
||||
.replace(/~~(.*?)~~/gim, '<del>$1</del>')
|
||||
// Code blocks
|
||||
.replace(/```([\s\S]*?)```/gim, '<pre class="bg-gray-800 text-green-400 p-2 rounded text-sm my-2"><code>$1</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`(.*?)`/gim, '<code class="bg-gray-700 text-gray-200 px-1 rounded text-sm">$1</code>')
|
||||
// Lists
|
||||
.replace(/^\• (.*$)/gim, '<li class="ml-4">• $1</li>')
|
||||
// Line breaks
|
||||
.replace(/\n/gim, '<br>');
|
||||
|
||||
return { __html: html };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Ładowanie edytora wiadomości...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center mb-6">
|
||||
<button
|
||||
onClick={() => navigate('/servers')}
|
||||
className="discord-button secondary mr-4 flex items-center"
|
||||
>
|
||||
<ArrowLeft size={16} className="mr-2" />
|
||||
Powrót do serwerów
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Edytor wiadomości</h1>
|
||||
<p className="text-gray-600">
|
||||
Edytuj wiadomość powitalną dla serwera
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="success">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Editor Panel */}
|
||||
<div className="discord-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Edytor</h3>
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<span>{message.content.length}/2000</span>
|
||||
{isDirty && (
|
||||
<span className="text-orange-600 font-medium">
|
||||
• Niezapisane zmiany
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={message.content}
|
||||
onChange={(e) => setMessage({ ...message, content: e.target.value })}
|
||||
className="discord-textarea"
|
||||
placeholder="Wprowadź treść wiadomości powitalnej...
|
||||
|
||||
Możesz używać Discord Markdown:
|
||||
# Nagłówek 1
|
||||
## Nagłówek 2
|
||||
**Pogrubienie**
|
||||
*Kursywa*
|
||||
__Podkreślenie__
|
||||
~~Przekreślenie~~
|
||||
`kod`
|
||||
```
|
||||
blok kodu
|
||||
```
|
||||
• Lista
|
||||
• Punktowana"
|
||||
style={{ height: '400px' }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={resetMessage}
|
||||
disabled={!isDirty}
|
||||
className="discord-button secondary flex items-center"
|
||||
>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Resetuj
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={saveMessage}
|
||||
disabled={saving || !message.content.trim() || !isDirty}
|
||||
className="discord-button flex items-center"
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
{saving ? 'Zapisywanie...' : 'Zapisz'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Panel */}
|
||||
<div className="discord-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Podgląd</h3>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setPreviewMode('discord')}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
previewMode === 'discord'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
Discord
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPreviewMode('markdown')}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
previewMode === 'markdown'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
Markdown
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="preview-area" style={{ height: '400px', overflowY: 'auto' }}>
|
||||
{message.content ? (
|
||||
previewMode === 'discord' ? (
|
||||
<div dangerouslySetInnerHTML={renderDiscordPreview(message.content)} />
|
||||
) : (
|
||||
<ReactMarkdown>{message.content}</ReactMarkdown>
|
||||
)
|
||||
) : (
|
||||
<p className="text-gray-400 italic">
|
||||
Wprowadź treść wiadomości, aby zobaczyć podgląd
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tips */}
|
||||
<div className="discord-card mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
💡 Wskazówki dotyczące formatowania
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Podstawowe formatowanie:</h4>
|
||||
<ul className="space-y-1 text-gray-600">
|
||||
<li><code>**tekst**</code> → <strong>pogrubienie</strong></li>
|
||||
<li><code>*tekst*</code> → <em>kursywa</em></li>
|
||||
<li><code>__tekst__</code> → <u>podkreślenie</u></li>
|
||||
<li><code>~~tekst~~</code> → <del>przekreślenie</del></li>
|
||||
<li><code>`kod`</code> → kod wewnątrz tekstu</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Zaawansowane:</h4>
|
||||
<ul className="space-y-1 text-gray-600">
|
||||
<li><code># Nagłówek</code> → duży nagłówek</li>
|
||||
<li><code>## Nagłówek</code> → średni nagłówek</li>
|
||||
<li><code>### Nagłówek</code> → mały nagłówek</li>
|
||||
<li><code>• Element</code> → lista punktowana</li>
|
||||
<li><code>```kod```</code> → blok kodu</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character limit warning */}
|
||||
{message.content.length > 1800 && (
|
||||
<div className="discord-card mt-4 border-l-4 border-orange-400 bg-orange-50">
|
||||
<div className="flex items-center">
|
||||
<div className="text-orange-600 mr-3">⚠️</div>
|
||||
<div>
|
||||
<p className="font-medium text-orange-800">
|
||||
Uwaga: Zbliżasz się do limitu znaków
|
||||
</p>
|
||||
<p className="text-sm text-orange-700">
|
||||
Discord ogranicza wiadomości do 2000 znaków.
|
||||
Pozostało: {2000 - message.content.length} znaków
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageEditor;
|
||||
221
bot/frontend/src/pages/ServerSelect.js
Normal file
221
bot/frontend/src/pages/ServerSelect.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '../services/api';
|
||||
import { Server, MessageSquare, Settings, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
function ServerSelect() {
|
||||
const [guilds, setGuilds] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadGuilds();
|
||||
}, []);
|
||||
|
||||
const loadGuilds = async () => {
|
||||
try {
|
||||
const response = await api.getGuilds();
|
||||
setGuilds(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading guilds:', error);
|
||||
setError('Nie udało się załadować listy serwerów');
|
||||
|
||||
// Mock data dla rozwoju
|
||||
setGuilds([
|
||||
{
|
||||
id: '123456789',
|
||||
name: 'Test Server',
|
||||
icon: null,
|
||||
hasBot: true,
|
||||
userPermissions: ['MANAGE_CHANNELS'],
|
||||
memberCount: 150,
|
||||
welcomeChannelConfigured: true
|
||||
},
|
||||
{
|
||||
id: '987654321',
|
||||
name: 'Drugi Serwer',
|
||||
icon: null,
|
||||
hasBot: false,
|
||||
userPermissions: ['MANAGE_CHANNELS'],
|
||||
memberCount: 50,
|
||||
welcomeChannelConfigured: false
|
||||
},
|
||||
{
|
||||
id: '555666777',
|
||||
name: 'Gaming Community',
|
||||
icon: null,
|
||||
hasBot: true,
|
||||
userPermissions: ['ADMINISTRATOR'],
|
||||
memberCount: 300,
|
||||
welcomeChannelConfigured: false
|
||||
}
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getServerIcon = (guild) => {
|
||||
if (guild.icon) {
|
||||
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getServerInitials = (name) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(word => word[0])
|
||||
.join('')
|
||||
.substring(0, 2)
|
||||
.toUpperCase();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Ładowanie serwerów...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Twoje serwery</h1>
|
||||
<p className="text-gray-600">
|
||||
Wybierz serwer, aby zarządzać wiadomościami powaitalnymi
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{guilds.length === 0 ? (
|
||||
<div className="discord-card text-center">
|
||||
<Server className="mx-auto text-gray-400 mb-4" size={48} />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Brak dostępnych serwerów
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Nie znaleziono serwerów z odpowiednimi uprawnieniami.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Upewnij się, że masz uprawnienia administracyjne na serwerze i że bot jest zaproszony.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{guilds.map((guild) => (
|
||||
<div key={guild.id} className="guild-card">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<div className="guild-icon">
|
||||
{getServerIcon(guild) ? (
|
||||
<img
|
||||
src={getServerIcon(guild)}
|
||||
alt={guild.name}
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
getServerInitials(guild.name)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900">{guild.name}</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{guild.memberCount || 0} członków
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end space-y-1">
|
||||
<div className="flex items-center">
|
||||
{guild.hasBot ? (
|
||||
<CheckCircle className="text-green-600" size={16} />
|
||||
) : (
|
||||
<XCircle className="text-red-600" size={16} />
|
||||
)}
|
||||
<span className={`text-xs ml-1 ${
|
||||
guild.hasBot ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{guild.hasBot ? 'Bot aktywny' : 'Bot nieaktywny'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{guild.welcomeChannelConfigured && (
|
||||
<div className="flex items-center">
|
||||
<MessageSquare className="text-blue-600" size={16} />
|
||||
<span className="text-xs text-blue-600 ml-1">
|
||||
Kanał skonfigurowany
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{guild.hasBot ? (
|
||||
<>
|
||||
<Link
|
||||
to={`/servers/${guild.id}/messages`}
|
||||
className="discord-button w-full flex items-center justify-center"
|
||||
>
|
||||
<MessageSquare size={16} className="mr-2" />
|
||||
Zarządzaj wiadomościami
|
||||
</Link>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className="discord-button secondary flex-1 flex items-center justify-center text-sm"
|
||||
disabled
|
||||
>
|
||||
<Settings size={14} className="mr-1" />
|
||||
Ustawienia
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Bot nie jest zaproszony na ten serwer
|
||||
</p>
|
||||
<button
|
||||
className="discord-button w-full"
|
||||
onClick={() => {
|
||||
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.REACT_APP_DISCORD_CLIENT_ID}&permissions=2147484672&scope=bot%20applications.commands&guild_id=${guild.id}`;
|
||||
window.open(inviteUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
Zaproś bota
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<div className="text-xs text-gray-500">
|
||||
Uprawnienia: {guild.userPermissions?.includes('ADMINISTRATOR')
|
||||
? 'Administrator'
|
||||
: guild.userPermissions?.join(', ') || 'Brak'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="discord-card mt-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Nie widzisz swojego serwera?
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
<p>• Upewnij się, że masz uprawnienia administracyjne na serwerze</p>
|
||||
<p>• Sprawdź czy bot jest zaproszony na serwer z odpowiednimi uprawnieniami</p>
|
||||
<p>• Wyloguj się i zaloguj ponownie, aby odświeżyć listę serwerów</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServerSelect;
|
||||
95
bot/frontend/src/services/api.js
Normal file
95
bot/frontend/src/services/api.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import axios from 'axios';
|
||||
|
||||
class ApiService {
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: process.env.NODE_ENV === 'production'
|
||||
? '/api'
|
||||
: 'http://localhost:3000/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
setupInterceptors() {
|
||||
// Request interceptor
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setAuthToken(token) {
|
||||
if (token) {
|
||||
this.client.defaults.headers.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
delete this.client.defaults.headers.Authorization;
|
||||
}
|
||||
}
|
||||
|
||||
// Auth endpoints
|
||||
async verifyToken() {
|
||||
return this.client.get('/auth/verify');
|
||||
}
|
||||
|
||||
async logout() {
|
||||
return this.client.post('/auth/logout');
|
||||
}
|
||||
|
||||
// Guild endpoints
|
||||
async getGuilds() {
|
||||
return this.client.get('/guilds');
|
||||
}
|
||||
|
||||
async getGuildConfig(guildId) {
|
||||
return this.client.get(`/guilds/${guildId}/config`);
|
||||
}
|
||||
|
||||
// Message endpoints
|
||||
async getMessage(guildId) {
|
||||
return this.client.get(`/messages/${guildId}`);
|
||||
}
|
||||
|
||||
async saveMessage(guildId, content, embedData = null) {
|
||||
return this.client.post(`/messages/${guildId}`, {
|
||||
content,
|
||||
embed_data: embedData
|
||||
});
|
||||
}
|
||||
|
||||
async getMessageRevisions(guildId) {
|
||||
return this.client.get(`/messages/${guildId}/revisions`);
|
||||
}
|
||||
|
||||
// Health check
|
||||
async healthCheck() {
|
||||
return this.client.get('/health');
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ApiService();
|
||||
Reference in New Issue
Block a user