first commit

This commit is contained in:
2025-07-21 00:45:28 +02:00
parent ab10b0f9a1
commit 93232a1663
39 changed files with 4860 additions and 0 deletions

46
bot/frontend/package.json Normal file
View 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"
}

View 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>

View 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
View 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
View 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;

View 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;

View 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>
);
}

View 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
View 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>
);

View 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;

View 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;

View 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;

View 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;

View 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();