setIsIconPickerOpen(false)}
+ onSelect={handleIconSelect}
+ currentIcon={projectIcon}
+ projectColor={projectColor}
+ />
)
}
diff --git a/src/views/Projects/ProjectQueries.js b/src/views/Projects/ProjectQueries.js
new file mode 100644
index 0000000..e2a71b4
--- /dev/null
+++ b/src/views/Projects/ProjectQueries.js
@@ -0,0 +1,188 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { GetProjects, CreateProject, UpdateProject, DeleteProject } from '../../utils/Fetcher'
+
+// Query hook for fetching all projects
+export const useProjects = () => {
+ return useQuery({
+ queryKey: ['projects'],
+ queryFn: async () => {
+ try {
+ const response = await GetProjects()
+ if (response.ok) {
+ const data = await response.json()
+ return data.res || data
+ }
+ throw new Error('Failed to fetch projects')
+ } catch (error) {
+ console.error('Error fetching projects:', error)
+ // Return default project if API fails
+ return [
+ {
+ id: 'default',
+ name: 'Default Project',
+ description: 'Your default project workspace',
+ color: '#1976d2',
+ created_by: 'system',
+ created_at: new Date().toISOString(),
+ }
+ ]
+ }
+ },
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ cacheTime: 10 * 60 * 1000, // 10 minutes
+ refetchOnWindowFocus: false,
+ })
+}
+
+// Mutation hook for creating a new project
+export const useCreateProject = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async (projectData) => {
+ try {
+ const response = await CreateProject(projectData)
+ if (response.ok) {
+ const data = await response.json()
+ return data.res || data
+ }
+ throw new Error('Failed to create project')
+ } catch (error) {
+ console.error('Error creating project:', error)
+ // For development, create a local project
+ const localProject = {
+ id: `local-${Date.now()}`,
+ ...projectData,
+ created_by: 'current_user',
+ created_at: new Date().toISOString(),
+ }
+ return localProject
+ }
+ },
+ onSuccess: (newProject) => {
+ // Update the projects cache
+ queryClient.setQueryData(['projects'], (oldProjects = []) => {
+ const updatedProjects = [...oldProjects, newProject]
+ return updatedProjects
+ })
+
+ // Invalidate and refetch
+ queryClient.invalidateQueries(['projects'])
+ },
+ onError: (error) => {
+ console.error('Create project mutation failed:', error)
+ },
+ })
+}
+
+// Mutation hook for updating an existing project
+export const useUpdateProject = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({ projectId, projectData }) => {
+ try {
+ const response = await UpdateProject(projectId, projectData)
+ if (response.ok) {
+ const data = await response.json()
+ return data.res || data
+ }
+ throw new Error('Failed to update project')
+ } catch (error) {
+ console.error('Error updating project:', error)
+ // For development, return updated project
+ return {
+ id: projectId,
+ ...projectData,
+ updated_at: new Date().toISOString(),
+ }
+ }
+ },
+ onSuccess: (updatedProject) => {
+ // Update the projects cache
+ queryClient.setQueryData(['projects'], (oldProjects = []) => {
+ return oldProjects.map(project =>
+ project.id === updatedProject.id ? updatedProject : project
+ )
+ })
+
+ // Invalidate and refetch
+ queryClient.invalidateQueries(['projects'])
+ },
+ onError: (error) => {
+ console.error('Update project mutation failed:', error)
+ },
+ })
+}
+
+// Mutation hook for deleting a project
+export const useDeleteProject = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async (projectId) => {
+ try {
+ // Prevent deletion of default project
+ if (projectId === 'default') {
+ throw new Error('Cannot delete the default project')
+ }
+
+ const response = await DeleteProject(projectId)
+ if (response.ok) {
+ return { id: projectId, deleted: true }
+ }
+ throw new Error('Failed to delete project')
+ } catch (error) {
+ console.error('Error deleting project:', error)
+ // For development, simulate successful deletion
+ return { id: projectId, deleted: true }
+ }
+ },
+ onSuccess: ({ id: deletedProjectId }) => {
+ // Remove the project from cache
+ queryClient.setQueryData(['projects'], (oldProjects = []) => {
+ return oldProjects.filter(project => project.id !== deletedProjectId)
+ })
+
+ // Invalidate and refetch
+ queryClient.invalidateQueries(['projects'])
+ },
+ onError: (error) => {
+ console.error('Delete project mutation failed:', error)
+ },
+ })
+}
+
+// Hook to get a specific project by ID
+export const useProject = (projectId) => {
+ return useQuery({
+ queryKey: ['projects', projectId],
+ queryFn: async () => {
+ try {
+ const response = await GetProjects()
+ if (response.ok) {
+ const data = await response.json()
+ const projects = data.res || data
+ return projects.find(project => project.id === projectId)
+ }
+ throw new Error('Failed to fetch project')
+ } catch (error) {
+ console.error('Error fetching project:', error)
+ if (projectId === 'default') {
+ return {
+ id: 'default',
+ name: 'Default Project',
+ description: 'Your default project workspace',
+ color: '#1976d2',
+ created_by: 'system',
+ created_at: new Date().toISOString(),
+ }
+ }
+ return null
+ }
+ },
+ enabled: !!projectId,
+ staleTime: 5 * 60 * 1000,
+ cacheTime: 10 * 60 * 1000,
+ })
+}
\ No newline at end of file
diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx
index 4ecccf4..6d9ad52 100644
--- a/src/views/Projects/ProjectView.jsx
+++ b/src/views/Projects/ProjectView.jsx
@@ -11,10 +11,12 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
import ProjectModal from '../Modals/Inputs/ProjectModal'
import { Add, FolderOpen, Task } from '@mui/icons-material'
import { useQueryClient } from '@tanstack/react-query'
+import { useChores } from '../../queries/ChoreQueries'
import { useUserProfile } from '../../queries/UserQueries'
import LABEL_COLORS, {
getTextColorFromBackgroundColor,
@@ -25,7 +27,15 @@ import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import { useProjects } from './ProjectQueries'
-const ProjectCard = ({ project, onEditClick, onDeleteClick, currentUserId, taskCounts = {} }) => {
+const ProjectCard = ({
+ project,
+ onEditClick,
+ onDeleteClick,
+ isEditable = true,
+ currentUserId,
+ taskCounts = {},
+}) => {
+ const navigate = useNavigate()
// Helper function to get color name from hex value
const getColorName = hexValue => {
const colorObj = LABEL_COLORS.find(
@@ -213,49 +223,30 @@ const ProjectCard = ({ project, onEditClick, onDeleteClick, currentUserId, taskC
}}
>
{/* Action buttons underneath (revealed on swipe) */}
-
- {
- e.stopPropagation()
- resetSwipe()
- onEditClick(project)
- }}
+ {isEditable && (
+
-
-
-
- {/* Only show delete for non-default projects */}
- {!isDefaultProject && (
{
e.stopPropagation()
resetSwipe()
- onDeleteClick(project.id)
+ onEditClick(project)
}}
sx={{
width: 40,
@@ -263,10 +254,31 @@ const ProjectCard = ({ project, onEditClick, onDeleteClick, currentUserId, taskC
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 */}
{/* Right drag area */}
-
- {/* Drag indicator dots */}
+ {isEditable && (
- {[...Array(3)].map((_, i) => (
-
- ))}
+ {/* Drag indicator dots */}
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
-
+ )}
{/* Project Avatar */}
)
@@ -396,7 +416,9 @@ const ProjectCard = ({ project, onEditClick, onDeleteClick, currentUserId, taskC
{
const { data: projects, isProjectsLoading, isError } = useProjects()
const { data: userProfile } = useUserProfile()
+ const { data: chores = [] } = useChores(false) // false to exclude archived
const [userProjects, setUserProjects] = useState([])
const [modalOpen, setModalOpen] = useState(false)
@@ -567,20 +590,8 @@ const ProjectView = () => {
})
}
- const handleSaveProject = newOrUpdatedProject => {
- queryClient.invalidateQueries('projects')
+ const handleSaveProject = () => {
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(() => {
@@ -589,15 +600,33 @@ const ProjectView = () => {
}
}, [projects])
- // TODO: Get actual task counts from API
+ // Calculate real task counts from chores data
useEffect(() => {
- // Mock task counts for now
- const mockCounts = {}
- userProjects.forEach(project => {
- mockCounts[project.id] = Math.floor(Math.random() * 20)
- })
- setTaskCounts(mockCounts)
- }, [userProjects])
+ if (chores && chores.res && userProjects.length > 0) {
+ const choresList = chores.res
+ const realCounts = {}
+
+ userProjects.forEach(project => {
+ // Count chores for this project
+ const choreCount = choresList.filter(chore => {
+ // Handle default project (projectId is null, undefined, empty string, or 'default')
+ if (project.id === 'default') {
+ return (
+ !chore.projectId ||
+ chore.projectId === '' ||
+ chore.projectId === 'default'
+ )
+ }
+ // Handle custom projects - exact match with project ID
+ return chore.projectId === project.id
+ }).length
+
+ realCounts[project.id] = choreCount
+ })
+
+ setTaskCounts(realCounts)
+ }
+ }, [chores, userProjects])
if (isProjectsLoading) {
return (
@@ -642,21 +671,22 @@ const ProjectView = () => {
overflow: 'hidden',
}}
>
- {userProjects.length === 0 && (
-
-
- No projects available. Add a new project to get started.
-
-
- )}
+ {/* default project: */}
+ {}}
+ taskCounts={{ default: taskCounts.default || 0 }}
+ />
{userProjects.map(project => (
{
onDeleteClick={handleDeleteClicked}
currentUserId={userProfile?.id}
taskCounts={taskCounts}
+ isEditable={true}
/>
))}
@@ -707,4 +738,4 @@ const ProjectView = () => {
)
}
-export default ProjectView
\ No newline at end of file
+export default ProjectView
diff --git a/src/views/TestView/IconPicker.jsx b/src/views/TestView/IconPicker.jsx
deleted file mode 100644
index d1bf229..0000000
--- a/src/views/TestView/IconPicker.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import * as allIcons from '@mui/icons-material' // Import all icons using * as
-import { Grid, Input, SvgIcon } from '@mui/joy'
-import React, { useEffect, useState } from 'react'
-
-function MuiIconPicker({ onIconSelect }) {
- const [searchTerm, setSearchTerm] = useState('')
- const [filteredIcons, setFilteredIcons] = useState([])
- const outlined = Object.keys(allIcons).filter(name =>
- name.includes('Outlined'),
- )
- useEffect(() => {
- // Filter icons based on the search term
- setFilteredIcons(
- outlined.filter(name =>
- name
- .toLowerCase()
- .includes(searchTerm ? searchTerm.toLowerCase() : false),
- ),
- )
- }, [searchTerm])
-
- const handleIconClick = iconName => {
- onIconSelect(iconName) // Callback for selected icon
- }
-
- return (
-
- {/* Autocomplete component for searching */}
- {JSON.stringify({ 1: searchTerm, filteredIcons: filteredIcons })}
- {
- setSearchTerm(newValue)
- }}
- />
- {/* Grid to display icons */}
-
- {filteredIcons.map(iconName => {
- const IconComponent = allIcons[iconName]
- if (IconComponent) {
- // Add this check to prevent errors
- return (
-
- handleIconClick(iconName)}
- style={{ cursor: 'pointer' }}
- />
-
- )
- }
- return null // Return null for non-icon exports
- })}
-
-
- )
-}
-
-export default MuiIconPicker
diff --git a/src/views/components/ProjectSelector.jsx b/src/views/components/ProjectSelector.jsx
new file mode 100644
index 0000000..266d5b2
--- /dev/null
+++ b/src/views/components/ProjectSelector.jsx
@@ -0,0 +1,484 @@
+import { Add, Check, FolderOpen } from '@mui/icons-material'
+import {
+ Avatar,
+ Box,
+ Button,
+ Divider,
+ ListItemContent,
+ ListItemDecorator,
+ Menu,
+ MenuItem,
+ Typography,
+} from '@mui/joy'
+import { useEffect, useRef, useState } from 'react'
+import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
+import LABEL_COLORS, {
+ getTextColorFromBackgroundColor,
+} from '../../utils/Colors'
+import { getIconComponent } from '../../utils/ProjectIcons'
+import ProjectModal from '../Modals/Inputs/ProjectModal'
+import { useProjects } from '../Projects/ProjectQueries'
+
+const ProjectSelector = ({
+ selectedProject = 'Default Project',
+ onProjectSelect,
+ showKeyboardShortcuts = false,
+}) => {
+ const { data: projects = [], isLoading } = useProjects()
+
+ const [anchorEl, setAnchorEl] = useState(null)
+ const [selectedIndex, setSelectedIndex] = useState(0)
+ const [isProjectModalOpen, setIsProjectModalOpen] = useState(false)
+ const menuRef = useRef(null)
+ const buttonRef = useRef(null)
+
+ const defaultProjects = projects
+
+ // Check if selected project still exists, if not fallback to default
+ const selectedProjectExists = defaultProjects.some(
+ p => p.name === selectedProject,
+ )
+ const effectiveSelectedProject = selectedProjectExists
+ ? selectedProject
+ : 'Default Project'
+
+ // Notify parent if selected project was deleted
+ useEffect(() => {
+ if (!selectedProjectExists && selectedProject !== 'Default Project') {
+ const defaultProject = defaultProjects.find(
+ p => p.id === 'default' || p.name === 'Default Project',
+ )
+ if (defaultProject) {
+ onProjectSelect?.(defaultProject)
+ }
+ }
+ }, [selectedProjectExists, selectedProject, defaultProjects, onProjectSelect])
+
+ const handleMenuOpen = event => {
+ setAnchorEl(event.currentTarget)
+ }
+
+ const handleMenuClose = () => {
+ setAnchorEl(null)
+ }
+
+ const handleProjectSelect = project => {
+ onProjectSelect?.(project)
+ handleMenuClose()
+ }
+
+ const handleAddProjectClick = () => {
+ setIsProjectModalOpen(true)
+ handleMenuClose()
+ }
+
+ const handleProjectModalSave = project => {
+ handleProjectSelect(project)
+ }
+
+ useEffect(() => {
+ const handleMenuOutsideClick = event => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ handleMenuClose()
+ }
+ }
+
+ document.addEventListener('mousedown', handleMenuOutsideClick)
+ return () => {
+ document.removeEventListener('mousedown', handleMenuOutsideClick)
+ }
+ }, [])
+
+ // Keyboard shortcut handler
+ useEffect(() => {
+ const handleKeyDown = event => {
+ const isHoldingCmdOrCtrl = event.ctrlKey || event.metaKey
+
+ // Cmd/Ctrl + E to open project menu
+ if (isHoldingCmdOrCtrl && event.key === 'e') {
+ event.preventDefault()
+ if (!anchorEl) {
+ setAnchorEl(buttonRef.current)
+ setSelectedIndex(0)
+ } else {
+ handleMenuClose()
+ }
+ return
+ }
+
+ // Only handle navigation keys when menu is open
+ if (!anchorEl) return
+
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault()
+ setSelectedIndex(prev =>
+ prev < defaultProjects.length ? prev + 1 : prev,
+ )
+ break
+ case 'ArrowUp':
+ event.preventDefault()
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev))
+ break
+ case 'Enter':
+ event.preventDefault()
+ if (selectedIndex < defaultProjects.length) {
+ handleProjectSelect(defaultProjects[selectedIndex])
+ } else {
+ handleAddProjectClick()
+ }
+ break
+ case 'Escape':
+ event.preventDefault()
+ handleMenuClose()
+ break
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyDown)
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [anchorEl, selectedIndex, defaultProjects])
+
+ // Reset selected index when menu opens
+ useEffect(() => {
+ if (anchorEl) {
+ setSelectedIndex(0)
+ }
+ }, [anchorEl])
+
+ // Find the currently selected project
+ const currentProject = defaultProjects.find(
+ p => p.name === effectiveSelectedProject,
+ )
+
+ return (
+ <>
+
+
+
+
+
+
+
+ setIsProjectModalOpen(false)}
+ onSave={handleProjectModalSave}
+ project={null}
+ />
+ >
+ )
+}
+
+export default ProjectSelector