responsive
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `${location.protocol}//${location.hostname}:${location.port}/api`,
|
||||
baseURL: `${location.protocol}//${location.hostname}:3000/api`,
|
||||
});
|
||||
|
||||
export default api;
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Box, Container, Grid, IconButton, Link, Stack } from '@mui/material';
|
||||
import { GitHub, YouTube, Telegram } from '@mui/icons-material';
|
||||
|
||||
export default function Footer() {
|
||||
interface FooterProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function Footer({ isMobile }: FooterProps) {
|
||||
return (
|
||||
<Box
|
||||
component="footer"
|
||||
@@ -19,12 +23,12 @@ export default function Footer() {
|
||||
<Grid container spacing={4} justifyContent="space-between" alignItems="center">
|
||||
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<img src="/img/logo.png" alt="Logo" width={32} height={32} style={{ marginRight: 14 }} />
|
||||
<Stack direction="row" alignItems="center" spacing={1} justifyContent={isMobile ? 'center' : 'start'}>
|
||||
<img src="/img/logo.png" alt="Logo" width={32} height={32} style={{ marginRight: isMobile ? 0 : 14 }} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: { xs: 'left', sm: 'center' } }}>
|
||||
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: 'center' }}>
|
||||
<Link
|
||||
href="https://3dp-manager.com/docs/intro"
|
||||
target="_blank"
|
||||
@@ -38,7 +42,7 @@ export default function Footer() {
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: { xs: 'left', sm: 'right' } }}>
|
||||
<Stack direction="row" spacing={1} justifyContent={{ xs: 'flex-start', sm: 'flex-end' }}>
|
||||
<Stack direction="row" spacing={1} justifyContent={{ xs: 'center', sm: 'flex-end' }}>
|
||||
|
||||
<IconButton
|
||||
component="a"
|
||||
|
||||
@@ -10,8 +10,14 @@ import {
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { Menu as MenuIcon } from '@mui/icons-material';
|
||||
|
||||
export default function Header() {
|
||||
interface HeaderProps {
|
||||
onMenuClick?: () => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function Header({ onMenuClick, isMobile }: HeaderProps) {
|
||||
const { mode, toggleColorMode } = useThemeContext();
|
||||
const { logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
@@ -48,12 +54,17 @@ export default function Header() {
|
||||
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<Toolbar>
|
||||
{isMobile && (
|
||||
<IconButton color="inherit" edge="start" onClick={onMenuClick} sx={{ mr: 1 }}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<img src="/img/logo.png" alt="Logo" width={32} height={32} style={{ marginRight: 14 }} />
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1, fontWeight: 'bold', color: '#1395de' }}>
|
||||
<Typography variant={isMobile ? 'body1' : 'h6'} noWrap component="div" sx={{ flexGrow: 1, fontWeight: 'bold', color: '#1395de' }}>
|
||||
3DP-MANAGER
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', gap: isMobile ? 0.25 : 1 }}>
|
||||
|
||||
<Tooltip title="Справка о программе">
|
||||
<IconButton color="inherit" onClick={() => setHelpOpen(true)}>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
Toolbar, Drawer, List, ListItem,
|
||||
ListItemButton, ListItemIcon, ListItemText, Box
|
||||
ListItemButton, ListItemIcon, ListItemText, Box, useMediaQuery, useTheme
|
||||
} from '@mui/material';
|
||||
import { People, Settings, Dns, SwapHoriz } from '@mui/icons-material';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
@@ -13,6 +14,13 @@ const drawerWidth = 240;
|
||||
export default function Layout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ text: 'Подписки', icon: <People />, path: '/' },
|
||||
@@ -21,35 +29,44 @@ export default function Layout() {
|
||||
{ text: 'Настройки', icon: <Settings />, path: '/settings' },
|
||||
];
|
||||
|
||||
const drawerContent = (
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<Toolbar />
|
||||
<List>
|
||||
{menuItems.map((item) => (
|
||||
<ListItem key={item.text} disablePadding>
|
||||
<ListItemButton
|
||||
selected={location.pathname === item.path}
|
||||
onClick={() => {
|
||||
navigate(item.path);
|
||||
if (isMobile) setMobileOpen(false);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', minHeight: '100vh', width: '100%' }}>
|
||||
|
||||
<Header />
|
||||
{/* Передаем функцию открытия в Header */}
|
||||
<Header onMenuClick={handleDrawerToggle} isMobile={isMobile} />
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
variant={isMobile ? "temporary" : "permanent"}
|
||||
open={isMobile ? mobileOpen : true}
|
||||
onClose={handleDrawerToggle}
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' },
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<List>
|
||||
{menuItems.map((item) => (
|
||||
<ListItem key={item.text} disablePadding>
|
||||
<ListItemButton
|
||||
selected={location.pathname === item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
|
||||
<Box
|
||||
@@ -59,16 +76,15 @@ export default function Layout() {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
|
||||
<Box sx={{ flexGrow: 1, p: 3 }}>
|
||||
<Box sx={{ flexGrow: 1, p: { xs: 2, md: 3 } }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
|
||||
<Footer />
|
||||
<Footer isMobile={isMobile} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
.animated-container {
|
||||
background: linear-gradient(-45deg, #111827, #15122c, #24243e, #0B0F19);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientBG 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientBG {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
input:autofill {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Box, TextField, Button, Typography, List, ListItem, ListItemText, IconButton, Paper, TablePagination } from '@mui/material';
|
||||
import { Box, TextField, Button, Typography, List, ListItem, ListItemText, IconButton, Paper, TablePagination, useTheme, useMediaQuery } from '@mui/material';
|
||||
import { Delete, Add, UploadFile, Remove } from '@mui/icons-material';
|
||||
import api from '../api';
|
||||
|
||||
@@ -10,6 +10,8 @@ export default function DomainsPage() {
|
||||
const [newDomain, setNewDomain] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
@@ -52,12 +54,10 @@ export default function DomainsPage() {
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
if (confirm('ВНИМАНИЕ! Вы действительно хотите удалить ВСЕ домены из белого списка?')) {
|
||||
if (confirm('Это действие необратимо. Точно удалить?')) {
|
||||
try {
|
||||
await api.delete('/domains/all');
|
||||
loadDomains();
|
||||
} catch (_e) { alert('Ошибка удаления'); }
|
||||
}
|
||||
try {
|
||||
await api.delete('/domains/all');
|
||||
loadDomains();
|
||||
} catch (_e) { alert('Ошибка удаления'); }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,11 +87,11 @@ export default function DomainsPage() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Белый список доменов (SNI)</Typography>
|
||||
<Typography variant={isMobile ? 'h5' : 'h4'} gutterBottom>Белый список доменов (SNI)</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="Добавить домен" size="small" fullWidth
|
||||
label="Доменное имя" size="small" fullWidth
|
||||
value={newDomain} onChange={(e) => setNewDomain(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
@@ -124,10 +124,9 @@ export default function DomainsPage() {
|
||||
Удалить все
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
)}
|
||||
|
||||
<Paper>
|
||||
<Paper sx={{ mt: domains.length > 0 ? 0 : 3 }}>
|
||||
<List>
|
||||
{domains.map((d) => (
|
||||
<ListItem key={d.id} secondaryAction={
|
||||
@@ -136,7 +135,7 @@ export default function DomainsPage() {
|
||||
<ListItemText primary={d.name} />
|
||||
</ListItem>
|
||||
))}
|
||||
{domains.length === 0 && <Typography sx={{ p: 2 }} color='textSecondary' textAlign='center'>Список пуст</Typography>}
|
||||
{domains.length === 0 && <Typography sx={{ p: 2 }} color='textSecondary' textAlign='center'>Нет доменов</Typography>}
|
||||
</List>
|
||||
<TablePagination
|
||||
component="div"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Paper, TextField, Button, Typography, Alert } from '@mui/material';
|
||||
import { Box, Paper, TextField, Button, Typography, Alert, Chip } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
@@ -22,12 +22,21 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
<Box className="animated-container" sx={{
|
||||
height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
bgcolor: 'background.default'
|
||||
}}>
|
||||
<Paper sx={{ p: 4, width: '100%', maxWidth: 400 }}>
|
||||
<Typography variant="h5" gutterBottom align="center">Вход в 3DP-MANAGER</Typography>
|
||||
<Paper sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
animation: 'fadeIn 1.5s ease-out',
|
||||
boxShadow: '0 15px 25px rgba(0,0,0,0.5)'
|
||||
}}>
|
||||
<Typography variant="h5" gutterBottom align="center"><span style={{ verticalAlign: 'middle' }}>Вход в 3DP-MANAGER</span> <Chip label="v2.0.1" size="small" sx={{ verticalAlign: 'middle' }} /></Typography>
|
||||
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
|
||||
@@ -40,7 +49,7 @@ export default function LoginPage() {
|
||||
fullWidth margin="normal" label="Пароль" type="password"
|
||||
value={creds.password} onChange={(e) => setCreds({ ...creds, password: e.target.value })}
|
||||
/>
|
||||
<Button fullWidth variant="contained" size="large" type="submit" sx={{ mt: 3 }}>
|
||||
<Button fullWidth variant="contained" size="large" type="submit" sx={{ mt: 3, transition: 'transform 0.3s ease', '&:hover': { transform: 'scale(1.05)' } }}>
|
||||
Войти
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton } from '@mui/material';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton, useTheme, useMediaQuery } from '@mui/material';
|
||||
import api from '../api';
|
||||
import { CheckCircle, PauseCircleFilled, PlayCircleFilled, Schedule, Update } from '@mui/icons-material';
|
||||
|
||||
@@ -27,6 +27,8 @@ export default function SettingsPage() {
|
||||
const [msg, setMsg] = useState({ open: false, type: 'success' as 'success' | 'error', text: '' });
|
||||
const [intervalError, setIntervalError] = useState<string>('');
|
||||
const [loadingRotate, setLoadingRotate] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@@ -190,7 +192,7 @@ export default function SettingsPage() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Настройки утилиты</Typography>
|
||||
<Typography variant={isMobile ? 'h5' : 'h4'} gutterBottom>Настройки утилиты</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
|
||||
@@ -276,7 +278,7 @@ export default function SettingsPage() {
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
sx={{ mt: 2, ml: 2 }}
|
||||
sx={{ mt: 2, ml: isMobile ? 1 : 2 }}
|
||||
onClick={handleCheckConnection}
|
||||
>
|
||||
Проверить
|
||||
@@ -322,7 +324,7 @@ export default function SettingsPage() {
|
||||
loading={loadingRotate}
|
||||
color="warning"
|
||||
onClick={handleForceRotate}
|
||||
sx={{ mt: 2, ml: 2 }}
|
||||
sx={{ mt: 2, ml: isMobile ? 0 : 2 }}
|
||||
>
|
||||
Сгенерировать сейчас
|
||||
</Button>
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
Select,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
type SelectChangeEvent
|
||||
type SelectChangeEvent,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { Delete, Add, Link as LinkIcon, Refresh, OpenInNew, ContentCopy, Dns, Router } from '@mui/icons-material';
|
||||
import { Delete, Add, Link as LinkIcon, OpenInNew, ContentCopy, Dns, Router } from '@mui/icons-material';
|
||||
import api from '../api';
|
||||
|
||||
interface Subscription {
|
||||
@@ -59,6 +61,8 @@ export default function SubscriptionsPage() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [selectedServer, setSelectedServer] = useState<string | number>('main');
|
||||
|
||||
@@ -111,7 +115,7 @@ export default function SubscriptionsPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Typography variant="h4">Подписки</Typography>
|
||||
<Typography variant={isMobile ? 'h5' : 'h4'}>Подписки</Typography>
|
||||
{tunnels.length > 0 && (
|
||||
<FormControl variant='standard' size="small" sx={{ minWidth: 220, justifyContent: 'center' }}>
|
||||
<Select
|
||||
@@ -141,7 +145,6 @@ export default function SubscriptionsPage() {
|
||||
</FormControl>
|
||||
)}
|
||||
<Box>
|
||||
<Button startIcon={<Refresh />} onClick={loadSubs} sx={{ mr: 1 }}>Обновить</Button>
|
||||
<Button variant="contained" startIcon={<Add />} onClick={() => setOpen(true)}>Создать</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -189,6 +192,7 @@ export default function SubscriptionsPage() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{subs.length === 0 && <Typography sx={{ p: 2 }} color='textSecondary' textAlign='center'>Нет подписок</Typography>}
|
||||
</Paper>
|
||||
|
||||
<Dialog open={open} onClose={() => setOpen(false)} disableRestoreFocus>
|
||||
|
||||
@@ -2,7 +2,9 @@ import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box, Button, Typography, Paper, Table, TableBody, TableCell,
|
||||
TableHead, TableRow, IconButton, Dialog, DialogTitle,
|
||||
DialogContent, TextField, DialogActions, Chip, CircularProgress
|
||||
DialogContent, TextField, DialogActions, Chip, CircularProgress,
|
||||
useTheme,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import { Delete, Add, Terminal, CheckCircle, Error, Dns } from '@mui/icons-material';
|
||||
import api from '../api';
|
||||
@@ -20,6 +22,8 @@ export default function TunnelsPage() {
|
||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loadingId, setLoadingId] = useState<number | null>(null);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: '', ip: '', sshPort: 22, username: 'root', password: '', domain: ''
|
||||
@@ -70,7 +74,7 @@ export default function TunnelsPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Typography variant="h4">Relay серверы</Typography>
|
||||
<Typography variant={isMobile ? 'h5' : 'h4'}>Relay серверы</Typography>
|
||||
<Button variant="contained" startIcon={<Add />} onClick={() => setOpen(true)}>Добавить</Button>
|
||||
</Box>
|
||||
|
||||
@@ -119,7 +123,7 @@ export default function TunnelsPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{tunnels.length === 0 && <TableRow><TableCell colSpan={4} align="center">Список пуст</TableCell></TableRow>}
|
||||
{tunnels.length === 0 && <TableRow><TableCell colSpan={4} align="center" sx={{ color: 'text.secondary' }}>Нет серверов</TableCell></TableRow>}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import type { PaletteMode } from '@mui/material';
|
||||
|
||||
const lightPalette = {
|
||||
primary: {
|
||||
main: '#2563eb',
|
||||
main: '#1395de',
|
||||
light: '#60a5fa',
|
||||
dark: '#1e40af',
|
||||
},
|
||||
@@ -21,7 +21,7 @@ const lightPalette = {
|
||||
|
||||
const darkPalette = {
|
||||
primary: {
|
||||
main: '#3b82f6',
|
||||
main: '#1395de',
|
||||
light: '#60a5fa',
|
||||
dark: '#1d4ed8',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user