feat(02-02): implement UI components for weekly calendar view
- Created TaskList component with WeeklyTaskItem - Created WeeklyCalendarView component with drag-and-drop functionality - Created main tasks page with integration - Added TaskForm component for creating new tasks - Implemented navigation controls for week switching - Added responsive styling for all components Files created: - src/components/TaskList.tsx - src/components/WeeklyCalendarView.tsx - src/app/tasks/page.tsx - src/components/TaskForm.tsx - src/components/WeeklyTaskItem.tsx - src/app/globals.css
This commit is contained in:
parent
b6c6ea94ef
commit
132f23a6ee
255
src/app/globals.css
Normal file
255
src/app/globals.css
Normal file
@ -0,0 +1,255 @@
|
||||
/* Global Styles */
|
||||
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Task List */
|
||||
.task-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Task Item */
|
||||
.task-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.task-item:hover {
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.task-item.completed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.task-item h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.task-item p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.time-indicator {
|
||||
font-size: 0.875rem;
|
||||
color: #3498db;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Weekly Calendar View */
|
||||
.weekly-calendar-view {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.week-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #ecf0f1;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background-color: #d5dbdb;
|
||||
}
|
||||
|
||||
.week-range {
|
||||
font-weight: 500;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.day-column {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.day-header {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day-content {
|
||||
padding: 1rem;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* Task Form */
|
||||
.task-form {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #27ae60;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #219653;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
.week-navigation {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.week-range {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
105
src/app/tasks/page.tsx
Normal file
105
src/app/tasks/page.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import WeeklyCalendarView from '@/components/WeeklyCalendarView';
|
||||
import TaskForm from '@/components/TaskForm';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completed: boolean;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
dayOfWeek?: number; // 0 = Sunday, 1 = Monday, etc.
|
||||
}
|
||||
|
||||
const TasksPage: React.FC = () => {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
// Simulate fetching tasks from API
|
||||
useEffect(() => {
|
||||
const fetchTasks = async () => {
|
||||
// In a real app, this would be an API call
|
||||
// const response = await fetch('/api/tasks');
|
||||
// const data = await response.json();
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockTasks: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Morning workout',
|
||||
description: '30 minute cardio session',
|
||||
completed: false,
|
||||
startTime: '07:00',
|
||||
endTime: '07:30',
|
||||
dayOfWeek: 1 // Monday
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Team meeting',
|
||||
description: 'Weekly sprint planning',
|
||||
completed: false,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
dayOfWeek: 2 // Tuesday
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Lunch with Sarah',
|
||||
description: 'Discuss project requirements',
|
||||
completed: false,
|
||||
startTime: '12:30',
|
||||
endTime: '13:30',
|
||||
dayOfWeek: 3 // Wednesday
|
||||
}
|
||||
];
|
||||
|
||||
setTasks(mockTasks);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchTasks();
|
||||
}, []);
|
||||
|
||||
const handleTaskDrop = (taskId: string, newDayOfWeek: number) => {
|
||||
setTasks(prevTasks =>
|
||||
prevTasks.map(task =>
|
||||
task.id === taskId ? { ...task, dayOfWeek: newDayOfWeek } : task
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddTask = (taskData: any) => {
|
||||
const newTask: Task = {
|
||||
id: `task-${Date.now()}`,
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
completed: false,
|
||||
startTime: taskData.startTime,
|
||||
endTime: taskData.endTime,
|
||||
dayOfWeek: taskData.dayOfWeek
|
||||
};
|
||||
|
||||
setTasks([...tasks, newTask]);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="tasks-page">
|
||||
<h1>Loading tasks...</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tasks-page">
|
||||
<h1>Weekly Tasks</h1>
|
||||
<TaskForm onSubmit={handleAddTask} />
|
||||
<WeeklyCalendarView tasks={tasks} onTaskDrop={handleTaskDrop} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TasksPage;
|
||||
121
src/components/TaskForm.tsx
Normal file
121
src/components/TaskForm.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface TaskFormData {
|
||||
title: string;
|
||||
description?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
dayOfWeek?: number;
|
||||
}
|
||||
|
||||
interface TaskFormProps {
|
||||
onSubmit: (task: TaskFormData) => void;
|
||||
}
|
||||
|
||||
const TaskForm: React.FC<TaskFormProps> = ({ onSubmit }) => {
|
||||
const [formData, setFormData] = useState<TaskFormData>({
|
||||
title: '',
|
||||
description: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
dayOfWeek: 1 // Default to Monday
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
// Reset form
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
dayOfWeek: 1
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="task-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="title">Task Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="startTime">Start Time</label>
|
||||
<input
|
||||
type="time"
|
||||
id="startTime"
|
||||
name="startTime"
|
||||
value={formData.startTime}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="endTime">End Time</label>
|
||||
<input
|
||||
type="time"
|
||||
id="endTime"
|
||||
name="endTime"
|
||||
value={formData.endTime}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="dayOfWeek">Day of Week</label>
|
||||
<select
|
||||
id="dayOfWeek"
|
||||
name="dayOfWeek"
|
||||
value={formData.dayOfWeek}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={0}>Sunday</option>
|
||||
<option value={1}>Monday</option>
|
||||
<option value={2}>Tuesday</option>
|
||||
<option value={3}>Wednesday</option>
|
||||
<option value={4}>Thursday</option>
|
||||
<option value={5}>Friday</option>
|
||||
<option value={6}>Saturday</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit-button">
|
||||
Add Task
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskForm;
|
||||
42
src/components/TaskList.tsx
Normal file
42
src/components/TaskList.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import WeeklyTaskItem from './WeeklyTaskItem';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completed: boolean;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
dayOfWeek?: number; // 0 = Sunday, 1 = Monday, etc.
|
||||
}
|
||||
|
||||
interface TaskListProps {
|
||||
tasks: Task[];
|
||||
onTaskClick?: (task: Task) => void;
|
||||
}
|
||||
|
||||
const TaskList: React.FC<TaskListProps> = ({ tasks, onTaskClick }) => {
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<p>No tasks found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="task-list">
|
||||
{tasks.map((task) => (
|
||||
<li key={task.id}>
|
||||
<WeeklyTaskItem
|
||||
task={task}
|
||||
onClick={() => onTaskClick?.(task)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskList;
|
||||
138
src/components/WeeklyCalendarView.tsx
Normal file
138
src/components/WeeklyCalendarView.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import TaskList from './TaskList';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completed: boolean;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
dayOfWeek?: number; // 0 = Sunday, 1 = Monday, etc.
|
||||
}
|
||||
|
||||
interface WeeklyCalendarViewProps {
|
||||
tasks: Task[];
|
||||
onTaskDrop?: (taskId: string, newDayOfWeek: number) => void;
|
||||
}
|
||||
|
||||
const WeeklyCalendarView: React.FC<WeeklyCalendarViewProps> = ({ tasks, onTaskDrop }) => {
|
||||
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(new Date());
|
||||
|
||||
// Days of the week
|
||||
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
// Function to get tasks for a specific day
|
||||
const getTasksForDay = (dayIndex: number): Task[] => {
|
||||
return tasks.filter(task => task.dayOfWeek === dayIndex);
|
||||
};
|
||||
|
||||
// Function to handle drag start
|
||||
const handleDragStart = (e: React.DragEvent, taskId: string) => {
|
||||
e.dataTransfer.setData('taskId', taskId);
|
||||
};
|
||||
|
||||
// Function to handle drag over
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Function to handle drop
|
||||
const handleDrop = (e: React.DragEvent, dayIndex: number) => {
|
||||
e.preventDefault();
|
||||
const taskId = e.dataTransfer.getData('taskId');
|
||||
if (onTaskDrop) {
|
||||
onTaskDrop(taskId, dayIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to goToPreviousWeek
|
||||
const goToPreviousWeek = () => {
|
||||
const newDate = new Date(currentWeekStart);
|
||||
newDate.setDate(newDate.getDate() - 7);
|
||||
setCurrentWeekStart(newDate);
|
||||
};
|
||||
|
||||
// Function to goToNextWeek
|
||||
const goToNextWeek = () => {
|
||||
const newDate = new Date(currentWeekStart);
|
||||
newDate.setDate(newDate.getDate() + 7);
|
||||
setCurrentWeekStart(newDate);
|
||||
};
|
||||
|
||||
// Get current week range
|
||||
const getWeekRange = (): { start: string; end: string } => {
|
||||
const start = new Date(currentWeekStart);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 6);
|
||||
|
||||
return {
|
||||
start: start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
end: end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
};
|
||||
};
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
goToPreviousWeek();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
goToNextWeek();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [currentWeekStart]);
|
||||
|
||||
const weekRange = getWeekRange();
|
||||
|
||||
return (
|
||||
<div className="weekly-calendar-view">
|
||||
<div className="calendar-header">
|
||||
<div className="week-navigation">
|
||||
<button
|
||||
onClick={goToPreviousWeek}
|
||||
className="nav-button"
|
||||
aria-label="Previous week"
|
||||
>
|
||||
← Previous Week
|
||||
</button>
|
||||
<span className="week-range">{weekRange.start} - {weekRange.end}</span>
|
||||
<button
|
||||
onClick={goToNextWeek}
|
||||
className="nav-button"
|
||||
aria-label="Next week"
|
||||
>
|
||||
Next Week →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="calendar-grid">
|
||||
{daysOfWeek.map((day, index) => (
|
||||
<div
|
||||
key={day}
|
||||
className="day-column"
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
>
|
||||
<div className="day-header">
|
||||
<h3>{day}</h3>
|
||||
</div>
|
||||
<div className="day-content">
|
||||
<TaskList tasks={getTasksForDay(index)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeeklyCalendarView;
|
||||
41
src/components/WeeklyTaskItem.tsx
Normal file
41
src/components/WeeklyTaskItem.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completed: boolean;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
dayOfWeek?: number; // 0 = Sunday, 1 = Monday, etc.
|
||||
}
|
||||
|
||||
interface WeeklyTaskItemProps {
|
||||
task: Task;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const WeeklyTaskItem: React.FC<WeeklyTaskItemProps> = ({ task, onClick }) => {
|
||||
const handleTaskClick = () => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`task-item ${task.completed ? 'completed' : ''} weekly-task`}
|
||||
onClick={handleTaskClick}
|
||||
>
|
||||
<h4>{task.title}</h4>
|
||||
{task.description && <p>{task.description}</p>}
|
||||
{task.startTime && task.endTime && (
|
||||
<div className="time-indicator">
|
||||
<span>{task.startTime} - {task.endTime}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeeklyTaskItem;
|
||||
Loading…
Reference in New Issue
Block a user