Create a frontend wireframe. #7
16 changed files with 1405 additions and 57 deletions
64
frontend/app/components/ErrorFallback.tsx
Normal file
64
frontend/app/components/ErrorFallback.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertTitle,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Refresh as RefreshIcon,
|
||||||
|
BugReport as BugReportIcon,
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
|
interface ErrorFallbackProps {
|
||||||
|
error: Error
|
||||||
|
resetError: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ErrorFallback({
|
||||||
|
error,
|
||||||
|
resetError,
|
||||||
|
}: ErrorFallbackProps) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||||
|
<Paper sx={{ p: 4, textAlign: 'center' }}>
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<BugReportIcon sx={{ fontSize: 64, color: 'error.main', mb: 2 }} />
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Something went wrong
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
An unexpected error occurred. Please try refreshing the page or
|
||||||
|
contact support if the problem persists.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Alert severity="error" sx={{ mb: 3, textAlign: 'left' }}>
|
||||||
|
<AlertTitle>Error Details</AlertTitle>
|
||||||
|
{error.message}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={resetError}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Reload Page
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
237
frontend/app/components/Layout.tsx
Normal file
237
frontend/app/components/Layout.tsx
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
AppBar,
|
||||||
|
Box,
|
||||||
|
CssBaseline,
|
||||||
|
Drawer,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Toolbar,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
Fab,
|
||||||
|
LinearProgress,
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Menu as MenuIcon,
|
||||||
|
Dashboard as DashboardIcon,
|
||||||
|
Add as AddIcon,
|
||||||
|
Settings as SettingsIcon,
|
||||||
|
DarkMode as DarkModeIcon,
|
||||||
|
LightMode as LightModeIcon,
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
|
const drawerWidth = 240
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Layout({ children, loading = false }: LayoutProps) {
|
||||||
|
const theme = useTheme()
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
|
const [darkMode, setDarkMode] = useState(false)
|
||||||
|
|
||||||
|
// Check system preference and localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem('theme')
|
||||||
|
const prefersDark = window.matchMedia(
|
||||||
|
'(prefers-color-scheme: dark)'
|
||||||
|
).matches
|
||||||
|
setDarkMode(savedTheme === 'dark' || (!savedTheme && prefersDark))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDrawerToggle = () => {
|
||||||
|
setMobileOpen(!mobileOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleThemeToggle = () => {
|
||||||
|
const newTheme = !darkMode
|
||||||
|
setDarkMode(newTheme)
|
||||||
|
localStorage.setItem('theme', newTheme ? 'dark' : 'light')
|
||||||
|
document.documentElement.classList.toggle('dark', newTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
text: 'Tasks',
|
||||||
|
icon: <DashboardIcon />,
|
||||||
|
path: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Settings',
|
||||||
|
icon: <SettingsIcon />,
|
||||||
|
path: '/settings',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const drawer = (
|
||||||
|
<div>
|
||||||
|
<Toolbar>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
noWrap
|
||||||
|
component="div"
|
||||||
|
sx={{ fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Captain's Log
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
<List>
|
||||||
|
{menuItems.map(item => (
|
||||||
|
<ListItem key={item.text} disablePadding>
|
||||||
|
<ListItemButton
|
||||||
|
component="a"
|
||||||
|
href={item.path}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
mx: 1,
|
||||||
|
my: 0.5,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main + '10',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ color: theme.palette.primary.main }}>
|
||||||
|
{item.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={item.text}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<CssBaseline />
|
||||||
|
|
||||||
|
{/* Loading indicator */}
|
||||||
|
{loading && (
|
||||||
|
<LinearProgress
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: theme.zIndex.appBar + 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* App Bar */}
|
||||||
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
sx={{
|
||||||
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||||
|
ml: { md: `${drawerWidth}px` },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={handleDrawerToggle}
|
||||||
|
sx={{ mr: 2, display: { md: 'none' } }}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
Captain's Log
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
onClick={handleThemeToggle}
|
||||||
|
aria-label="toggle dark mode"
|
||||||
|
>
|
||||||
|
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<Box
|
||||||
|
component="nav"
|
||||||
|
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
|
||||||
|
aria-label="navigation menu"
|
||||||
|
>
|
||||||
|
{/* Mobile drawer */}
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
open={mobileOpen}
|
||||||
|
onClose={handleDrawerToggle}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' },
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: drawerWidth,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
{/* Desktop drawer */}
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: drawerWidth,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
p: 3,
|
||||||
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||||
|
minHeight: '100vh',
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar />
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Quick capture FAB */}
|
||||||
|
<Fab
|
||||||
|
color="primary"
|
||||||
|
aria-label="add task"
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 16,
|
||||||
|
right: 16,
|
||||||
|
zIndex: theme.zIndex.fab,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</Fab>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
53
frontend/app/components/LoadingSpinner.tsx
Normal file
53
frontend/app/components/LoadingSpinner.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Box, CircularProgress, Typography, Skeleton } from '@mui/material'
|
||||||
|
|
||||||
|
interface LoadingSpinnerProps {
|
||||||
|
size?: number
|
||||||
|
message?: string
|
||||||
|
variant?: 'spinner' | 'skeleton'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoadingSpinner({
|
||||||
|
size = 40,
|
||||||
|
message = 'Loading...',
|
||||||
|
variant = 'spinner',
|
||||||
|
}: LoadingSpinnerProps) {
|
||||||
|
if (variant === 'skeleton') {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Skeleton variant="text" width="60%" height={32} sx={{ mb: 2 }} />
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
width="100%"
|
||||||
|
height={120}
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
width="100%"
|
||||||
|
height={120}
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
/>
|
||||||
|
<Skeleton variant="rectangular" width="100%" height={120} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 4,
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={size} thickness={4} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{message}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||||
import { ApiError } from '~/types/task'
|
import type { ApiError } from '~/types/task'
|
||||||
|
|
||||||
interface UseApiState<T> {
|
interface UseApiState<T> {
|
||||||
data: T | null
|
data: T | null
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
import { renderHook, act, waitFor } from '@testing-library/react'
|
import { renderHook, act, waitFor } from '@testing-library/react'
|
||||||
import { useTask } from './useTask'
|
import { useTask } from './useTask'
|
||||||
import { Task, TaskStatus, UpdateTaskRequest } from '~/types/task'
|
import type { Task, UpdateTaskRequest } from '~/types/task'
|
||||||
|
import { TaskStatus } from '~/types/task'
|
||||||
import { apiClient } from '~/services/api'
|
import { apiClient } from '~/services/api'
|
||||||
|
|
||||||
// Mock the API client
|
// Mock the API client
|
||||||
|
|
@ -13,7 +14,7 @@ vi.mock('~/services/api', () => ({
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockApiClient = apiClient as {
|
const mockApiClient = apiClient as unknown as {
|
||||||
getTask: ReturnType<typeof vi.fn>
|
getTask: ReturnType<typeof vi.fn>
|
||||||
updateTask: ReturnType<typeof vi.fn>
|
updateTask: ReturnType<typeof vi.fn>
|
||||||
deleteTask: ReturnType<typeof vi.fn>
|
deleteTask: ReturnType<typeof vi.fn>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { Task, UpdateTaskRequest, ApiError } from '~/types/task'
|
import type { Task, UpdateTaskRequest, ApiError } from '~/types/task'
|
||||||
import { apiClient } from '~/services/api'
|
import { apiClient } from '~/services/api'
|
||||||
|
|
||||||
interface UseTaskState {
|
interface UseTaskState {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
import { renderHook, act, waitFor } from '@testing-library/react'
|
import { renderHook, act, waitFor } from '@testing-library/react'
|
||||||
import { useTasks } from './useTasks'
|
import { useTasks } from './useTasks'
|
||||||
import { Task, TaskStatus, CreateTaskRequest } from '~/types/task'
|
import type { Task, CreateTaskRequest } from '~/types/task'
|
||||||
|
import { TaskStatus } from '~/types/task'
|
||||||
import { apiClient } from '~/services/api'
|
import { apiClient } from '~/services/api'
|
||||||
|
|
||||||
// Mock the API client
|
// Mock the API client
|
||||||
|
|
@ -12,7 +13,7 @@ vi.mock('~/services/api', () => ({
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockApiClient = apiClient as {
|
const mockApiClient = apiClient as unknown as {
|
||||||
listTasks: ReturnType<typeof vi.fn>
|
listTasks: ReturnType<typeof vi.fn>
|
||||||
createTask: ReturnType<typeof vi.fn>
|
createTask: ReturnType<typeof vi.fn>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
import { Task, CreateTaskRequest, TaskStatus, ApiError } from '~/types/task'
|
import type { Task, CreateTaskRequest, ApiError } from '~/types/task'
|
||||||
|
import { TaskStatus } from '~/types/task'
|
||||||
import { apiClient } from '~/services/api'
|
import { apiClient } from '~/services/api'
|
||||||
|
|
||||||
interface UseTasksState {
|
interface UseTasksState {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,12 @@ import {
|
||||||
Scripts,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
} from 'react-router'
|
} from 'react-router'
|
||||||
|
import { ThemeProvider } from '@mui/material/styles'
|
||||||
|
import { CssBaseline } from '@mui/material'
|
||||||
|
|
||||||
import type { Route } from './+types/root'
|
import type { Route } from './+types/root'
|
||||||
|
import { theme, darkTheme } from './theme'
|
||||||
|
import AppLayout from './components/Layout'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|
||||||
export const links: Route.LinksFunction = () => [
|
export const links: Route.LinksFunction = () => [
|
||||||
|
|
@ -42,7 +46,20 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return <Outlet />
|
const isDarkMode =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(localStorage.getItem('theme') === 'dark' ||
|
||||||
|
(!localStorage.getItem('theme') &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={isDarkMode ? darkTheme : theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<AppLayout>
|
||||||
|
<Outlet />
|
||||||
|
</AppLayout>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
|
@ -61,15 +78,26 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
stack = error.stack
|
stack = error.stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDarkMode =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(localStorage.getItem('theme') === 'dark' ||
|
||||||
|
(!localStorage.getItem('theme') &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="pt-16 p-4 container mx-auto">
|
<ThemeProvider theme={isDarkMode ? darkTheme : theme}>
|
||||||
<h1>{message}</h1>
|
<CssBaseline />
|
||||||
<p>{details}</p>
|
<AppLayout>
|
||||||
{stack && (
|
<main className="pt-16 p-4 container mx-auto">
|
||||||
<pre className="w-full p-4 overflow-x-auto">
|
<h1>{message}</h1>
|
||||||
<code>{stack}</code>
|
<p>{details}</p>
|
||||||
</pre>
|
{stack && (
|
||||||
)}
|
<pre className="w-full p-4 overflow-x-auto">
|
||||||
</main>
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</AppLayout>
|
||||||
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@ import { describe, it, expect } from 'vitest'
|
||||||
import Home from './home'
|
import Home from './home'
|
||||||
|
|
||||||
describe('Home component', () => {
|
describe('Home component', () => {
|
||||||
it('should render welcome component', () => {
|
it('should render task management interface', () => {
|
||||||
render(<Home />)
|
render(<Home />)
|
||||||
expect(screen.getByText(/React Router/i)).toBeInTheDocument()
|
expect(screen.getByText(/Tasks/i)).toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
screen.getByText(/GTD-inspired task management system/i)
|
||||||
|
).toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Task Management Interface Coming Soon/i)
|
||||||
|
).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,44 @@
|
||||||
import type { Route } from './+types/home'
|
import type { Route } from './+types/home'
|
||||||
import { Welcome } from '../welcome/welcome'
|
import { Box, Typography, Container } from '@mui/material'
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: 'New React Router App' },
|
{ title: "Captain's Log - Tasks" },
|
||||||
{ name: 'description', content: 'Welcome to React Router!' },
|
{ name: 'description', content: 'GTD-inspired task management system' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <Welcome />
|
return (
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<Box sx={{ py: 4 }}>
|
||||||
|
<Typography variant="h1" component="h1" gutterBottom>
|
||||||
|
Tasks
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
Your GTD-inspired task management system. Capture everything, see only
|
||||||
|
what matters.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'text.secondary',
|
||||||
|
border: '2px dashed',
|
||||||
|
borderColor: 'grey.300',
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Task Management Interface Coming Soon
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
The task list, task cards, and quick capture components will be
|
||||||
|
implemented in the next phase.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
import { apiClient } from './api'
|
import { apiClient } from './api'
|
||||||
import {
|
import type { Task, CreateTaskRequest, UpdateTaskRequest } from '~/types/task'
|
||||||
Task,
|
import { TaskStatus } from '~/types/task'
|
||||||
TaskStatus,
|
|
||||||
CreateTaskRequest,
|
|
||||||
UpdateTaskRequest,
|
|
||||||
} from '~/types/task'
|
|
||||||
|
|
||||||
// Mock fetch globally
|
// Mock fetch globally
|
||||||
const mockFetch = vi.fn()
|
const mockFetch = vi.fn()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {
|
import type {
|
||||||
Task,
|
Task,
|
||||||
CreateTaskRequest,
|
CreateTaskRequest,
|
||||||
UpdateTaskRequest,
|
UpdateTaskRequest,
|
||||||
|
|
|
||||||
280
frontend/app/theme.ts
Normal file
280
frontend/app/theme.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
import { createTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
declare module '@mui/material/styles' {
|
||||||
|
interface Theme {
|
||||||
|
custom: {
|
||||||
|
task: {
|
||||||
|
todo: string
|
||||||
|
done: string
|
||||||
|
backlog: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemeOptions {
|
||||||
|
custom?: {
|
||||||
|
task?: {
|
||||||
|
todo?: string
|
||||||
|
done?: string
|
||||||
|
backlog?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'light',
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
500: '#3b82f6',
|
||||||
|
main: '#3b82f6',
|
||||||
|
contrastText: '#ffffff',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#f9fafb',
|
||||||
|
paper: '#ffffff',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#111827',
|
||||||
|
secondary: '#6b7280',
|
||||||
|
},
|
||||||
|
grey: {
|
||||||
|
50: '#f9fafb',
|
||||||
|
100: '#f3f4f6',
|
||||||
|
200: '#e5e7eb',
|
||||||
|
300: '#d1d5db',
|
||||||
|
600: '#4b5563',
|
||||||
|
700: '#374151',
|
||||||
|
800: '#1f2937',
|
||||||
|
900: '#111827',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: '"Inter", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
h1: {
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
color: '#111827',
|
||||||
|
boxShadow:
|
||||||
|
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||||
|
borderBottom: '1px solid #e5e7eb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDrawer: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderRight: '1px solid #e5e7eb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: 'none',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow:
|
||||||
|
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
boxShadow:
|
||||||
|
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow:
|
||||||
|
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTextField: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
task: {
|
||||||
|
todo: '#3b82f6',
|
||||||
|
done: '#22c55e',
|
||||||
|
backlog: '#6b7280',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const darkTheme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
500: '#3b82f6',
|
||||||
|
main: '#3b82f6',
|
||||||
|
contrastText: '#ffffff',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#030712',
|
||||||
|
paper: '#1f2937',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#f9fafb',
|
||||||
|
secondary: '#9ca3af',
|
||||||
|
},
|
||||||
|
grey: {
|
||||||
|
50: '#f9fafb',
|
||||||
|
100: '#f3f4f6',
|
||||||
|
200: '#e5e7eb',
|
||||||
|
300: '#d1d5db',
|
||||||
|
600: '#4b5563',
|
||||||
|
700: '#374151',
|
||||||
|
800: '#1f2937',
|
||||||
|
900: '#111827',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: '"Inter", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
h1: {
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: '#1f2937',
|
||||||
|
color: '#f9fafb',
|
||||||
|
boxShadow:
|
||||||
|
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||||
|
borderBottom: '1px solid #374151',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDrawer: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundColor: '#1f2937',
|
||||||
|
borderRight: '1px solid #374151',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: 'none',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow:
|
||||||
|
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: '#1f2937',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
boxShadow:
|
||||||
|
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||||
|
border: '1px solid #374151',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow:
|
||||||
|
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||||
|
borderColor: '#4b5563',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTextField: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
task: {
|
||||||
|
todo: '#3b82f6',
|
||||||
|
done: '#22c55e',
|
||||||
|
backlog: '#6b7280',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
700
frontend/package-lock.json
generated
700
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -16,6 +16,10 @@
|
||||||
"typecheck": "react-router typegen && tsc"
|
"typecheck": "react-router typegen && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.3.2",
|
||||||
|
"@mui/material": "^7.3.2",
|
||||||
"@react-router/node": "^7.7.1",
|
"@react-router/node": "^7.7.1",
|
||||||
"@react-router/serve": "^7.7.1",
|
"@react-router/serve": "^7.7.1",
|
||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue