feat: Implement Keywords management page with DataGrid
Frontend Phase 2 - Keywords Management: - Add MainLayout component with sidebar navigation - Implement Keywords page with MUI DataGrid - Add Keywords CRUD operations (Create, Edit, Delete dialogs) - Add search and filter functionality (Category, Status) - Install @mui/x-data-grid package for table component - Update routing to include Keywords page - Update Dashboard to use MainLayout - Add navigation menu items for all planned pages Features implemented: - Keywords list with DataGrid table - Add/Edit keyword dialog with form validation - Delete confirmation dialog - Category filter (People, Topics, Companies) - Status filter (Active, Inactive) - Search functionality - Priority management Tested in browser: - Page loads successfully - API integration working (200 OK) - Layout and navigation functional - All UI components rendering correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,175 @@
|
||||
import { ReactNode, useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
List,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Button,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Dashboard as DashboardIcon,
|
||||
Label as KeywordIcon,
|
||||
AccountTree as PipelineIcon,
|
||||
People as PeopleIcon,
|
||||
Apps as AppsIcon,
|
||||
Article as ArticleIcon,
|
||||
BarChart as MonitoringIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '../stores/authStore'
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
text: string
|
||||
icon: ReactNode
|
||||
path: string
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
|
||||
{ text: 'Keywords', icon: <KeywordIcon />, path: '/keywords' },
|
||||
{ text: 'Pipelines', icon: <PipelineIcon />, path: '/pipelines' },
|
||||
{ text: 'Users', icon: <PeopleIcon />, path: '/users' },
|
||||
{ text: 'Applications', icon: <AppsIcon />, path: '/applications' },
|
||||
{ text: 'Articles', icon: <ArticleIcon />, path: '/articles' },
|
||||
{ text: 'Monitoring', icon: <MonitoringIcon />, path: '/monitoring' },
|
||||
]
|
||||
|
||||
export default function MainLayout({ children }: MainLayoutProps) {
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { user, logout } = useAuthStore()
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen)
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
navigate(path)
|
||||
setMobileOpen(false)
|
||||
}
|
||||
|
||||
const drawer = (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" noWrap component="div">
|
||||
News Engine
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List>
|
||||
{menuItems.map((item) => (
|
||||
<ListItem key={item.text} disablePadding>
|
||||
<ListItemButton
|
||||
selected={location.pathname === item.path}
|
||||
onClick={() => handleNavigate(item.path)}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
ml: { sm: `${drawerWidth}px` },
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ mr: 2, display: { sm: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
News Engine Console
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography variant="body2">
|
||||
{user?.full_name} ({user?.role})
|
||||
</Typography>
|
||||
<Button color="inherit" onClick={handleLogout}>
|
||||
Logout
|
||||
</Button>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
|
||||
aria-label="navigation"
|
||||
>
|
||||
{/* Mobile drawer */}
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', sm: 'none' },
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
{/* Desktop drawer */}
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</Box>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3,
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
minHeight: '100vh',
|
||||
bgcolor: '#f5f5f5',
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user