diff --git a/src/views/Modals/Inputs/ProjectModal.jsx b/src/views/Modals/Inputs/ProjectModal.jsx
new file mode 100644
index 0000000..ffbb562
--- /dev/null
+++ b/src/views/Modals/Inputs/ProjectModal.jsx
@@ -0,0 +1,367 @@
+import {
+ Avatar,
+ Box,
+ Button,
+ FormControl,
+ FormLabel,
+ Grid,
+ Input,
+ Stack,
+ Textarea,
+ Typography,
+} from '@mui/joy'
+import { useEffect, useState } from 'react'
+import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
+import LABEL_COLORS, {
+ getTextColorFromBackgroundColor,
+} from '../../../utils/Colors'
+import { CreateProject, UpdateProject } from '../../../utils/Fetcher'
+import PROJECT_ICONS, { getIconComponent } from '../../../utils/ProjectIcons'
+
+const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
+ const { ResponsiveModal } = useResponsiveModal()
+ const [projectName, setProjectName] = useState('')
+ const [projectDescription, setProjectDescription] = useState('')
+ const [projectColor, setProjectColor] = useState(LABEL_COLORS[0].value)
+ const [projectIcon, setProjectIcon] = useState(PROJECT_ICONS[0].value)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [error, setError] = useState('')
+
+ // Initialize form when modal opens or project changes
+ useEffect(() => {
+ if (isOpen) {
+ if (project) {
+ // Editing existing project
+ setProjectName(project.name || '')
+ setProjectDescription(project.description || '')
+ setProjectColor(project.color || LABEL_COLORS[0].value)
+ setProjectIcon(project.icon || PROJECT_ICONS[0].value)
+ } else {
+ // Creating new project
+ setProjectName('')
+ setProjectDescription('')
+ setProjectColor(LABEL_COLORS[0].value)
+ setProjectIcon(PROJECT_ICONS[0].value)
+ }
+ setError('')
+ setIsSubmitting(false)
+ }
+ }, [isOpen, project])
+
+ const handleSubmit = async e => {
+ e.preventDefault()
+
+ if (!projectName.trim()) {
+ setError('Project name is required')
+ return
+ }
+
+ setIsSubmitting(true)
+ setError('')
+
+ try {
+ const projectData = {
+ name: projectName.trim(),
+ description: projectDescription.trim(),
+ color: projectColor,
+ icon: projectIcon,
+ }
+
+ let response
+ if (project) {
+ // Update existing project
+ response = await UpdateProject(project.id, projectData)
+ } else {
+ // Create new project
+ response = await CreateProject(projectData)
+ }
+
+ if (response.ok) {
+ const savedProject = await response.json()
+ onSave(savedProject.res || savedProject)
+ onClose()
+ } else {
+ const errorData = await response.json()
+ setError(errorData.message || 'Failed to save project')
+ }
+ } catch (error) {
+ console.error('Error saving project:', error)
+ setError('An unexpected error occurred')
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ onClose()
+ }
+ }
+
+ return (
+
+
+ {project ? 'Edit Project' : 'Create New Project'}
+
+
+
+
+ )
+}
+
+export default ProjectModal
diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx
new file mode 100644
index 0000000..4ecccf4
--- /dev/null
+++ b/src/views/Projects/ProjectView.jsx
@@ -0,0 +1,710 @@
+import DeleteIcon from '@mui/icons-material/Delete'
+import EditIcon from '@mui/icons-material/Edit'
+import {
+ Avatar,
+ Box,
+ Chip,
+ CircularProgress,
+ Container,
+ IconButton,
+ Stack,
+ Typography,
+} from '@mui/joy'
+import { useEffect, useRef, useState } from 'react'
+import ProjectModal from '../Modals/Inputs/ProjectModal'
+
+import { Add, FolderOpen, Task } from '@mui/icons-material'
+import { useQueryClient } from '@tanstack/react-query'
+import { useUserProfile } from '../../queries/UserQueries'
+import LABEL_COLORS, {
+ getTextColorFromBackgroundColor,
+} from '../../utils/Colors'
+import { DeleteProject } from '../../utils/Fetcher'
+import { getIconComponent } from '../../utils/ProjectIcons'
+import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
+import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
+import { useProjects } from './ProjectQueries'
+
+const ProjectCard = ({ project, onEditClick, onDeleteClick, currentUserId, taskCounts = {} }) => {
+ // Helper function to get color name from hex value
+ const getColorName = hexValue => {
+ const colorObj = LABEL_COLORS.find(
+ color => color.value.toLowerCase() === hexValue.toLowerCase(),
+ )
+ return colorObj ? colorObj.name : hexValue
+ }
+
+ // Check if current user owns this project
+ const isOwnedByCurrentUser = project.created_by === currentUserId
+ const isDefaultProject = project.id === 'default'
+ const taskCount = taskCounts[project.id] || 0
+
+ // Swipe functionality state
+ const [swipeTranslateX, setSwipeTranslateX] = useState(0)
+ const [isDragging, setIsDragging] = useState(false)
+ const [isSwipeRevealed, setIsSwipeRevealed] = useState(false)
+ const [hoverTimer, setHoverTimer] = useState(null)
+ const swipeThreshold = 80
+ const maxSwipeDistance = 160
+ const dragStartX = useRef(0)
+ const cardRef = useRef(null)
+
+ // Swipe gesture handlers (same as LabelView)
+ const handleTouchStart = e => {
+ dragStartX.current = e.touches[0].clientX
+ setIsDragging(true)
+ }
+
+ const handleTouchMove = e => {
+ if (!isDragging) return
+
+ const currentX = e.touches[0].clientX
+ const deltaX = currentX - dragStartX.current
+
+ if (isSwipeRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ setSwipeTranslateX(clampedDelta)
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ setSwipeTranslateX(clampedDelta)
+ }
+ }
+ }
+
+ const handleTouchEnd = () => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ if (isSwipeRevealed) {
+ if (swipeTranslateX > -swipeThreshold) {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ } else {
+ setSwipeTranslateX(-maxSwipeDistance)
+ }
+ } else {
+ if (Math.abs(swipeTranslateX) > swipeThreshold) {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ } else {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+ }
+ }
+
+ const handleMouseDown = e => {
+ dragStartX.current = e.clientX
+ setIsDragging(true)
+ }
+
+ const handleMouseMove = e => {
+ if (!isDragging) return
+
+ const currentX = e.clientX
+ const deltaX = currentX - dragStartX.current
+
+ if (isSwipeRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ setSwipeTranslateX(clampedDelta)
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ setSwipeTranslateX(clampedDelta)
+ }
+ }
+ }
+
+ const handleMouseUp = () => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ if (isSwipeRevealed) {
+ if (swipeTranslateX > -swipeThreshold) {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ } else {
+ setSwipeTranslateX(-maxSwipeDistance)
+ }
+ } else {
+ if (Math.abs(swipeTranslateX) > swipeThreshold) {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ } else {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+ }
+ }
+
+ const resetSwipe = () => {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+
+ // Hover functionality for desktop
+ const handleMouseEnter = () => {
+ if (isSwipeRevealed) return
+ const timer = setTimeout(() => {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ setHoverTimer(null)
+ }, 800)
+ setHoverTimer(timer)
+ }
+
+ const handleMouseLeave = () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ if (!isSwipeRevealed) {
+ const hideTimer = setTimeout(() => {
+ resetSwipe()
+ }, 300)
+ setHoverTimer(hideTimer)
+ }
+ }
+
+ const handleActionAreaMouseEnter = () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ }
+
+ const handleActionAreaMouseLeave = () => {
+ if (isSwipeRevealed) {
+ resetSwipe()
+ }
+ }
+
+ // Clean up timer on unmount
+ useEffect(() => {
+ return () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ }
+ }
+ }, [hoverTimer])
+
+ return (
+
+ {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ }}
+ >
+ {/* Action buttons underneath (revealed on swipe) */}
+
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onEditClick(project)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 1,
+ }}
+ >
+
+
+
+ {/* Only show delete for non-default projects */}
+ {!isDefaultProject && (
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onDeleteClick(project.id)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 1,
+ }}
+ >
+
+
+ )}
+
+
+ {/* Main card content */}
+ {
+ if (isSwipeRevealed) {
+ resetSwipe()
+ return
+ }
+ onEditClick(project)
+ }}
+ onTouchStart={handleTouchStart}
+ onTouchMove={handleTouchMove}
+ onTouchEnd={handleTouchEnd}
+ onMouseDown={handleMouseDown}
+ onMouseMove={handleMouseMove}
+ onMouseUp={handleMouseUp}
+ >
+ {/* Right drag area */}
+
+ {/* Drag indicator dots */}
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+ {/* Project Avatar */}
+
+
+ {project.icon ? (
+ (() => {
+ const IconComponent = getIconComponent(project.icon)
+ return (
+
+ )
+ })()
+ ) : (
+
+ {project.name.charAt(0).toUpperCase()}
+
+ )}
+
+
+
+ {/* Content - Center */}
+
+ {/* Project Name */}
+
+ {project.name}
+ {isDefaultProject && (
+
+ Default
+
+ )}
+
+
+ {/* Project Info */}
+
+ {project.description && (
+
+ {project.description}
+
+ )}
+
+ }
+ sx={{
+ fontSize: 10,
+ height: 18,
+ px: 0.75,
+ bgcolor: 'primary.softBg',
+ color: 'primary.500',
+ }}
+ >
+ {taskCount} tasks
+
+
+ {project.color && (
+ }
+ sx={{
+ fontSize: 10,
+ height: 18,
+ px: 0.75,
+ bgcolor: `${project.color}20`,
+ color: project.color,
+ border: `1px solid ${project.color}30`,
+ }}
+ >
+ {getColorName(project.color)}
+
+ )}
+
+ {!isOwnedByCurrentUser && !isDefaultProject && (
+
+ Shared
+
+ )}
+
+
+
+
+
+ )
+}
+
+const ProjectView = () => {
+ const { data: projects, isProjectsLoading, isError } = useProjects()
+ const { data: userProfile } = useUserProfile()
+
+ const [userProjects, setUserProjects] = useState([])
+ const [modalOpen, setModalOpen] = useState(false)
+ const [currentProject, setCurrentProject] = useState(null)
+ const [taskCounts, setTaskCounts] = useState({})
+ const queryClient = useQueryClient()
+ const [confirmationModel, setConfirmationModel] = useState({})
+
+ const handleAddProject = () => {
+ setCurrentProject(null)
+ setModalOpen(true)
+ }
+
+ const handleEditProject = project => {
+ setCurrentProject(project)
+ setModalOpen(true)
+ }
+
+ const handleDeleteClicked = id => {
+ const project = userProjects.find(p => p.id === id)
+ setConfirmationModel({
+ isOpen: true,
+ title: 'Delete Project',
+ message: `Are you sure you want to delete "${project?.name}"? This will remove the project but keep all tasks (they'll move to the Default Project).`,
+ confirmText: 'Delete',
+ color: 'danger',
+ cancelText: 'Cancel',
+ onClose: confirmed => {
+ if (confirmed === true) {
+ handleDeleteProject(id)
+ }
+ setConfirmationModel({})
+ },
+ })
+ }
+
+ const handleDeleteProject = id => {
+ DeleteProject(id).then(() => {
+ const updatedProjects = userProjects.filter(project => project.id !== id)
+ setUserProjects(updatedProjects)
+ queryClient.invalidateQueries('projects')
+ })
+ }
+
+ const handleSaveProject = newOrUpdatedProject => {
+ queryClient.invalidateQueries('projects')
+ setModalOpen(false)
+
+ if (currentProject) {
+ // Update existing project
+ const updatedProjects = userProjects.map(project =>
+ project.id === newOrUpdatedProject.id ? newOrUpdatedProject : project,
+ )
+ setUserProjects(updatedProjects)
+ } else {
+ // Add new project
+ setUserProjects([...userProjects, newOrUpdatedProject])
+ }
+ }
+
+ useEffect(() => {
+ if (projects) {
+ setUserProjects(projects)
+ }
+ }, [projects])
+
+ // TODO: Get actual task counts from API
+ useEffect(() => {
+ // Mock task counts for now
+ const mockCounts = {}
+ userProjects.forEach(project => {
+ mockCounts[project.id] = Math.floor(Math.random() * 20)
+ })
+ setTaskCounts(mockCounts)
+ }, [userProjects])
+
+ if (isProjectsLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (isError) {
+ return (
+
+ Failed to load projects. Please try again.
+
+ )
+ }
+
+ return (
+
+
+
+
+ Projects
+
+
+ Organize your tasks into projects. Create custom workspaces to keep
+ your tasks organized and easily accessible.
+
+
+
+
+
+ {userProjects.length === 0 && (
+
+
+ No projects available. Add a new project to get started.
+
+
+ )}
+ {userProjects.map(project => (
+
+ ))}
+
+
+ {modalOpen && (
+ setModalOpen(false)}
+ onSave={handleSaveProject}
+ project={currentProject}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ProjectView
\ No newline at end of file