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:
mARTin 2026-01-25 11:25:03 +01:00
parent b6c6ea94ef
commit 132f23a6ee
6 changed files with 702 additions and 0 deletions

255
src/app/globals.css Normal file
View 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
View 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
View 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;

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

View 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"
>
&larr; Previous Week
</button>
<span className="week-range">{weekRange.start} - {weekRange.end}</span>
<button
onClick={goToNextWeek}
className="nav-button"
aria-label="Next week"
>
Next Week &rarr;
</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;

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