@@ -104,102 +100,83 @@ const ForgotPasswordView = () => {
padding: 2,
borderRadius: '8px',
boxShadow: 'md',
- minHeight: '70vh',
- justifyContent: 'space-between',
- justifyItems: 'center',
}}
>
-
-
- {/* */}
-
- Done
-
- tick
-
-
-
- {/* HERE */}
-
+
+
+
+ Done
+ tick
+
{resetStatusOk === null && (
-
- )}
- {resetStatusOk != null && (
<>
-
-
- if there is an account associated with the email you entered,
- you will receive an email with instructions on how to reset
- your
-
-
+
+ Enter your email, and we'll send you a link to get into your
+ account.
+
+
+ {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ handleSubmit()
+ }
+ }}
+ />
+ {emailError}
+
+
+ Reset Password
+
+
+ {
navigate('/login')
}}
+ color='neutral'
+ >
+ Back to Login
+
+ >
+ )}
+ {resetStatusOk != null && (
+ <>
+
+ If there is an account associated with the email you entered,
+ you will receive an email with instructions on how to reset
+ your password.
+
+
+ {
+ navigate('/login')
+ }}
>
Go to Login
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 15fd8f4..217ca19 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -1,3 +1,4 @@
+import { Browser } from '@capacitor/browser'
import { Capacitor } from '@capacitor/core'
import { Device } from '@capacitor/device'
// import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth'
@@ -251,19 +252,52 @@ const LoginView = () => {
return randomState
}
- const handleAuthentikLogin = () => {
+ const handleAuthentikLogin = async () => {
const authentikAuthorizeUrl = resource?.identity_provider?.auth_url
+ const state = generateRandomState()
- const params = new URLSearchParams({
- response_type: 'code',
- client_id: resource?.identity_provider?.client_id,
- redirect_uri: `${window.location.origin}/auth/oauth2`,
- scope: 'openid profile email', // Your scopes
- state: generateRandomState(),
- })
- console.log('redirect', `${authentikAuthorizeUrl}?${params.toString()}`)
+ if (Capacitor.isNativePlatform()) {
+ // For mobile devices, use a custom URL scheme for the redirect
+ const redirectUri = 'donetick://auth/oauth2'
+
+ const params = new URLSearchParams({
+ response_type: 'code',
+ client_id: resource?.identity_provider?.client_id,
+ redirect_uri: redirectUri,
+ scope: 'openid profile email',
+ state: state,
+ })
- window.location.href = `${authentikAuthorizeUrl}?${params.toString()}`
+ const authUrl = `${authentikAuthorizeUrl}?${params.toString()}`
+ console.log('Opening OAuth in browser:', authUrl)
+
+ try {
+ // Open OAuth flow in system browser
+ await Browser.open({ url: authUrl })
+
+ // Note: The OAuth callback will be handled by deep link handling
+ // You'll need to implement deep link handling to catch the redirect
+ // and extract the authorization code
+ } catch (error) {
+ console.error('Failed to open OAuth browser:', error)
+ showError({
+ title: 'OAuth Error',
+ message: 'Failed to open authentication browser',
+ })
+ }
+ } else {
+ // For web platforms, use the current approach
+ const params = new URLSearchParams({
+ response_type: 'code',
+ client_id: resource?.identity_provider?.client_id,
+ redirect_uri: `${window.location.origin}/auth/oauth2`,
+ scope: 'openid profile email',
+ state: state,
+ })
+
+ console.log('redirect', `${authentikAuthorizeUrl}?${params.toString()}`)
+ window.location.href = `${authentikAuthorizeUrl}?${params.toString()}`
+ }
}
return (
diff --git a/src/views/Authorization/Signup.jsx b/src/views/Authorization/Signup.jsx
index 58ba90d..3838601 100644
--- a/src/views/Authorization/Signup.jsx
+++ b/src/views/Authorization/Signup.jsx
@@ -11,6 +11,7 @@ import {
} from '@mui/joy'
import React from 'react'
import { useNavigate } from 'react-router-dom'
+import { useQueryClient } from '@tanstack/react-query'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { login, signUp } from '../../utils/Fetcher'
@@ -19,6 +20,7 @@ const SignupView = () => {
const [username, setUsername] = React.useState('')
const [password, setPassword] = React.useState('')
const Navigate = useNavigate()
+ const queryClient = useQueryClient()
const [displayName, setDisplayName] = React.useState('')
const [email, setEmail] = React.useState('')
const [usernameError, setUsernameError] = React.useState('')
@@ -32,11 +34,11 @@ const SignupView = () => {
response.json().then(res => {
localStorage.setItem('ca_token', res.token)
localStorage.setItem('ca_expiration', res.expire)
- setTimeout(() => {
- // TODO: not sure if there is a race condition here
- // but on first sign up it renavigates to login.
- Navigate('/chores')
- }, 500)
+
+ // Invalidate user profile queries to ensure fresh data
+ queryClient.invalidateQueries(['userProfile'])
+
+ Navigate('/chores')
})
} else {
console.log('Login failed', response)
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index e359f77..03668f1 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -117,7 +117,13 @@ const MyChores = () => {
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
useEffect(() => {
;(async () => {
- if (!choresLoading && !membersLoading && userProfile) {
+ if (
+ !choresLoading &&
+ !membersLoading &&
+ userProfile &&
+ membersData?.res &&
+ choresData?.res
+ ) {
setPerformers(membersData.res)
const sortedChores = choresData.res.sort(ChoreSorter)
setChores(sortedChores)
diff --git a/src/views/Chores/Sidepanel.jsx b/src/views/Chores/Sidepanel.jsx
index f879c70..e374b28 100644
--- a/src/views/Chores/Sidepanel.jsx
+++ b/src/views/Chores/Sidepanel.jsx
@@ -3,13 +3,16 @@ import { useMediaQuery } from '@mui/material'
import { useEffect, useState } from 'react'
import { useChoresHistory } from '../../queries/ChoreQueries'
import { ChoresGrouper } from '../../utils/Chores'
+import { getSidepanelConfig } from '../../utils/SidepanelConfig'
import CalendarView from '../components/CalendarView'
import ActivitiesCard from './ActivitesCard'
+import TasksByAssigneeCard from './TasksByAssigneeCard'
import WelcomeCard from './WelcomeCard'
const Sidepanel = ({ chores }) => {
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
const [dueDatePieChartData, setDueDatePieChartData] = useState([])
+ const [sidepanelConfig, setSidepanelConfig] = useState([])
const {
data: choresHistory,
isChoresHistoryLoading,
@@ -18,6 +21,18 @@ const Sidepanel = ({ chores }) => {
useEffect(() => {
setDueDatePieChartData(generateChoreDuePieChartData(chores))
+ setSidepanelConfig(getSidepanelConfig())
+ }, [])
+
+ useEffect(() => {
+ const handleConfigChange = () => {
+ setSidepanelConfig(getSidepanelConfig())
+ }
+
+ window.addEventListener('sidepanelConfigChanged', handleConfigChange)
+ return () => {
+ window.removeEventListener('sidepanelConfigChanged', handleConfigChange)
+ }
}, [])
const generateChoreDuePieChartData = chores => {
@@ -34,34 +49,60 @@ const Sidepanel = ({ chores }) => {
.filter(item => item.value > 0)
}
+ const renderCard = cardConfig => {
+ if (!cardConfig.enabled) return null
+
+ switch (cardConfig.id) {
+ case 'welcome':
+ return
+ case 'assignees':
+ return
+ case 'calendar':
+ return (
+
+
+
+
+
+ )
+ case 'activities':
+ return (
+
+ )
+
+ default:
+ return null
+ }
+ }
+
if (!isLargeScreen) {
return null
}
- return (
-
-
-
-
-
-
-
-
-
- )
+
+ const sortedCards = [...sidepanelConfig].sort((a, b) => a.order - b.order)
+
+ return {sortedCards.map(cardConfig => renderCard(cardConfig))}
}
export default Sidepanel
diff --git a/src/views/Chores/TasksByAssigneeCard.jsx b/src/views/Chores/TasksByAssigneeCard.jsx
new file mode 100644
index 0000000..787992b
--- /dev/null
+++ b/src/views/Chores/TasksByAssigneeCard.jsx
@@ -0,0 +1,398 @@
+import { BarChart, Person } from '@mui/icons-material'
+import { Avatar, Box, Sheet, Typography } from '@mui/joy'
+import { useEffect, useState } from 'react'
+import { useCircleMembers } from '../../queries/UserQueries'
+import { TASK_COLOR } from '../../utils/Colors'
+import { resolvePhotoURL } from '../../utils/Helpers'
+
+const TasksByAssigneeCard = ({ chores = [] }) => {
+ const [assigneeData, setAssigneeData] = useState([])
+ const { data: circleMembersData, isLoading: isCircleMembersLoading } =
+ useCircleMembers()
+
+ useEffect(() => {
+ if (
+ !isCircleMembersLoading &&
+ circleMembersData?.res &&
+ chores.length > 0
+ ) {
+ const members = circleMembersData.res
+ const data = processTasksByAssignee(chores, members)
+ setAssigneeData(data)
+ }
+ }, [chores, circleMembersData, isCircleMembersLoading])
+
+ const processTasksByAssignee = (chores, members) => {
+ const assigneeStats = {}
+
+ // Initialize stats for all members
+ members.forEach(member => {
+ assigneeStats[member.userId] = {
+ id: member.userId,
+ name: member.displayName || member.name,
+ image: member.image,
+ inProgress: 0,
+ overdue: 0,
+ scheduled: 0,
+ pendingReview: 0,
+ total: 0,
+ }
+ })
+
+ // Count tasks by status for each assignee
+ chores.forEach(chore => {
+ if (chore.assignedTo && assigneeStats[chore.assignedTo]) {
+ const assignee = assigneeStats[chore.assignedTo]
+ assignee.total++
+
+ // Map chore status to our categories based on your system
+ if (chore.status === 3) {
+ // Pending approval/review
+ assignee.pendingReview++
+ } else if (chore.status === 1 || chore.status === 2) {
+ // In progress (started or paused)
+ assignee.inProgress++
+ } else if (
+ chore.nextDueDate &&
+ new Date(chore.nextDueDate) < new Date()
+ ) {
+ // Overdue - past due date
+ assignee.overdue++
+ } else {
+ // Scheduled/planned - future due date or no due date
+ assignee.scheduled++
+ }
+ }
+ })
+
+ // Filter out members with no tasks and sort by total tasks
+ return Object.values(assigneeStats)
+ .filter(assignee => assignee.total > 0)
+ .sort((a, b) => b.total - a.total)
+ }
+
+ const getStatusColor = status => {
+ switch (status) {
+ case 'inProgress':
+ return TASK_COLOR.IN_PROGRESS
+ case 'overdue':
+ return TASK_COLOR.OVERDUE
+ case 'scheduled':
+ return TASK_COLOR.COMPLETED
+ case 'pendingReview':
+ return TASK_COLOR.PENDING_REVIEW
+ default:
+ return TASK_COLOR.DEFAULT
+ }
+ }
+
+ const maxTasks = Math.max(...assigneeData.map(a => a.total), 1)
+
+ if (isCircleMembersLoading) {
+ return (
+
+
+ Loading tasks by assignee...
+
+
+ )
+ }
+
+ if (assigneeData.length === 0) {
+ return (
+
+
+
+ No assigned tasks found
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ Tasks by Assignee
+
+
+
+ {/* Legend */}
+
+ {[
+ {
+ key: 'inProgress',
+ label: 'In Progress',
+ color: getStatusColor('inProgress'),
+ },
+ {
+ key: 'overdue',
+ label: 'Overdue',
+ color: getStatusColor('overdue'),
+ },
+ {
+ key: 'scheduled',
+ label: 'Scheduled',
+ color: getStatusColor('scheduled'),
+ },
+ {
+ key: 'pendingReview',
+ label: 'Pending Review',
+ color: getStatusColor('pendingReview'),
+ },
+ ].map(status => (
+
+
+
+ {status.label}
+
+
+ ))}
+
+
+ {/* Chart Container */}
+
+ {/* Chart */}
+
+ {assigneeData.slice(0, 6).map((assignee, index) => {
+ const barHeight = Math.max((assignee.total / maxTasks) * 140, 8)
+
+ return (
+
+ {/* Avatar */}
+
+ {assignee.name?.charAt(0) || }
+
+
+ {/* Stacked bars */}
+
+ {/* Pending Review - bottom */}
+ {assignee.pendingReview > 0 && (
+
+ )}
+
+ {/* Scheduled */}
+ {assignee.scheduled > 0 && (
+
+ )}
+
+ {/* In Progress */}
+ {assignee.inProgress > 0 && (
+
+ )}
+
+ {/* Overdue - top */}
+ {assignee.overdue > 0 && (
+
+ )}
+
+
+ {/* Name */}
+
+
+ {assignee.name}
+
+
+
+ )
+ })}
+
+
+ {/* Y-axis labels */}
+
+ {[0, 20, 40, 60, 80, 100].map((value, index) => {
+ const yPosition = (value / 100) * 140
+ return (
+
+ {Math.round((value / 100) * maxTasks)}
+
+ )
+ })}
+
+
+
+ )
+}
+
+export default TasksByAssigneeCard
diff --git a/src/views/Settings/MFASettings.jsx b/src/views/Settings/MFASettings.jsx
index 1e3c560..355e596 100644
--- a/src/views/Settings/MFASettings.jsx
+++ b/src/views/Settings/MFASettings.jsx
@@ -210,7 +210,7 @@ const MFASettings = () => {
}
return (
-
+
Multi-Factor Authentication
diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx
index fd37852..b7cbedf 100644
--- a/src/views/Settings/Settings.jsx
+++ b/src/views/Settings/Settings.jsx
@@ -1,4 +1,5 @@
import { Capacitor } from '@capacitor/core'
+import { Refresh } from '@mui/icons-material'
import {
Box,
Button,
@@ -18,6 +19,7 @@ import {
import { Purchases } from '@revenuecat/purchases-capacitor'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
import RealTimeSettings from '../../components/RealTimeSettings'
import SubscriptionModal from '../../components/SubscriptionModal'
import Logo from '../../Logo'
@@ -45,12 +47,14 @@ import APITokenSettings from './APITokenSettings'
import MFASettings from './MFASettings'
import NotificationSetting from './NotificationSetting'
import ProfileSettings from './ProfileSettings'
+import SidepanelSettings from './SidepanelSettings'
import StorageSettings from './StorageSettings'
import ThemeToggle from './ThemeToggle'
const Settings = () => {
const { data: userProfile } = useUserProfile()
const { showNotification } = useNotification()
+ const navigate = useNavigate()
const [userCircles, setUserCircles] = useState([])
const [circleMemberRequests, setCircleMemberRequests] = useState([])
@@ -59,6 +63,8 @@ const Settings = () => {
const [webhookURL, setWebhookURL] = useState(null)
const [webhookError, setWebhookError] = useState(null)
const [isAdmin, setIsAdmin] = useState(false)
+ const [lastRefresh, setLastRefresh] = useState(null)
+ const [isRefreshing, setIsRefreshing] = useState(false)
const [changePasswordModal, setChangePasswordModal] = useState(false)
const [subscriptionModal, setSubscriptionModal] = useState(false)
@@ -88,6 +94,23 @@ const Settings = () => {
},
})
}
+ const refreshMemberRequests = async () => {
+ setIsRefreshing(true)
+ try {
+ const resp = await GetCircleMemberRequests()
+ const data = await resp.json()
+ setCircleMemberRequests(data.res ? data.res : [])
+ setLastRefresh(new Date())
+ } catch (error) {
+ showNotification({
+ type: 'error',
+ message: 'Failed to refresh member requests',
+ })
+ } finally {
+ setIsRefreshing(false)
+ }
+ }
+
useEffect(() => {
GetUserCircle().then(resp => {
resp.json().then(data => {
@@ -98,6 +121,7 @@ const Settings = () => {
GetCircleMemberRequests().then(resp => {
resp.json().then(data => {
setCircleMemberRequests(data.res ? data.res : [])
+ setLastRefresh(new Date())
})
})
GetAllCircleMembers().then(data => {
@@ -116,15 +140,35 @@ const Settings = () => {
}, [circleMembers, userProfile])
useEffect(() => {
- const hash = window.location.hash
- if (hash) {
- const sharingSection = document.getElementById(
- window.location.hash.slice(1),
- )
- if (sharingSection) {
- sharingSection.scrollIntoView({ behavior: 'smooth' })
+ const handleHashChange = () => {
+ const hash = window.location.hash
+ if (hash) {
+ // Small delay to ensure the component is fully rendered before scrolling
+ setTimeout(() => {
+ const section = document.getElementById(hash.slice(1))
+ if (section) {
+ // Get the element position and scroll with some offset for the title
+ const elementPosition = section.offsetTop
+ const offsetPosition = elementPosition - 20 // 20px padding above the title
+
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'instant', // Use 'smooth' for smooth scrolling
+ })
+ }
+ }, 500)
}
}
+
+ // Handle initial hash on mount
+ handleHashChange()
+
+ // Listen for hash changes
+ window.addEventListener('hashchange', handleHashChange)
+
+ return () => {
+ window.removeEventListener('hashchange', handleHashChange)
+ }
}, [])
const getSubscriptionDetails = () => {
@@ -172,10 +216,11 @@ const Settings = () => {
if (!userProfile) {
return
}
+
return (
-
+
Circle settings
@@ -406,9 +451,35 @@ const Settings = () => {
))}
- {circleMemberRequests.length > 0 && (
+
Circle Member Requests
- )}
+
+ {lastRefresh && (
+
+ Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
+
+ )}
+ :
+ }
+ >
+ {isRefreshing ? 'Refreshing...' : 'Refresh'}
+
+
+
+
{circleMemberRequests.map(request => (
@@ -729,7 +800,18 @@ const Settings = () => {
-
+
+
Sidepanel Customization
+
+
+ Customize the layout and visibility of cards in the sidepanel. the
+ section only available on large screen devices such as tablets and
+ desktops..
+
+
+
+
+
Theme preferences
diff --git a/src/views/Settings/SettingsOverview.jsx b/src/views/Settings/SettingsOverview.jsx
new file mode 100644
index 0000000..d099a2a
--- /dev/null
+++ b/src/views/Settings/SettingsOverview.jsx
@@ -0,0 +1,300 @@
+import {
+ AccountCircle,
+ Api,
+ Circle,
+ Notifications,
+ Palette,
+ Person,
+ Security,
+ Star,
+ Storage,
+ ViewSidebar,
+} from '@mui/icons-material'
+import {
+ Avatar,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Container,
+ Grid,
+ Typography,
+} from '@mui/joy'
+import { useNavigate } from 'react-router-dom'
+import { useUserProfile } from '../../queries/UserQueries'
+import { isPlusAccount } from '../../utils/Helpers'
+
+const SettingsOverview = () => {
+ const navigate = useNavigate()
+ const { data: userProfile } = useUserProfile()
+
+ const settingsCards = [
+ {
+ id: 'profile',
+ title: 'Profile Settings',
+ description:
+ 'Update your profile information, photo, display name, and timezone preferences.',
+ icon: ,
+ color: 'primary',
+ },
+ {
+ id: 'circle',
+ title: 'Circle Settings',
+ description:
+ 'Manage your circle, invite members, handle join requests, and configure webhooks.',
+ icon: ,
+ color: 'success',
+ },
+ {
+ id: 'account',
+ title: 'Account Settings',
+ description:
+ 'Manage your subscription, change password, and account deletion options.',
+ icon: ,
+ color: 'warning',
+ },
+ {
+ id: 'notifications',
+ title: 'Notifications',
+ description:
+ 'Configure push notifications, email alerts, and notification targets for tasks.',
+ icon: ,
+ color: 'info',
+ },
+ {
+ id: 'mfa',
+ title: 'Multi-Factor Authentication',
+ description:
+ 'Add an extra layer of security with MFA using authenticator apps.',
+ icon: ,
+ color: 'danger',
+ },
+ {
+ id: 'apitokens',
+ title: 'API Tokens',
+ description:
+ 'Generate and manage access tokens for third-party integrations and API access.',
+ icon: ,
+ color: 'neutral',
+ },
+ {
+ id: 'storage',
+ title: 'Storage Settings',
+ description:
+ 'Backup and restore your data, manage local storage and sync preferences.',
+ icon: ,
+ color: 'primary',
+ },
+ {
+ id: 'sidepanel',
+ title: 'Sidepanel Customization',
+ description:
+ 'Customize the layout and visibility of cards in the sidepanel interface.',
+ icon: ,
+ color: 'success',
+ },
+ {
+ id: 'theme',
+ title: 'Theme Preferences',
+ description:
+ 'Choose your preferred theme and configure dark/light mode settings.',
+ icon: ,
+ color: 'warning',
+ },
+ ]
+
+ const handleCardClick = settingId => {
+ navigate(`/settings/detailed#${settingId}`)
+ }
+
+ return (
+
+
+
+ Settings
+
+
+ Customize your experience and manage your account preferences
+
+
+
+ {/* Upgrade Card - Only show if user is not a Plus member */}
+ {userProfile && !isPlusAccount(userProfile) && (
+
+
+ navigate('/settings/detailed#account')}
+ >
+
+
+
+
+
+
+
+
+ Upgrade to Plus
+
+
+ Unlock powerful features to enhance your productivity
+
+
+ • Rich text descriptions
+ • Task notifications
+ • API integrations
+ • Advanced automation
+
+
+
+ {
+ e.stopPropagation()
+ navigate('/settings/detailed#account')
+ }}
+ >
+ Upgrade Now
+
+
+
+
+
+
+ )}
+
+
+ {settingsCards.map(setting => (
+
+ handleCardClick(setting.id)}
+ >
+
+
+
+ {setting.icon}
+
+
+ {setting.title}
+
+
+ {setting.description}
+
+
+
+
+
+ ))}
+
+
+ )
+}
+
+export default SettingsOverview
diff --git a/src/views/Settings/SidepanelSettings.jsx b/src/views/Settings/SidepanelSettings.jsx
new file mode 100644
index 0000000..80b72ba
--- /dev/null
+++ b/src/views/Settings/SidepanelSettings.jsx
@@ -0,0 +1,258 @@
+import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
+import {
+ CalendarMonth,
+ DragIndicator,
+ History,
+ Person,
+ Visibility,
+ VisibilityOff,
+ WavingHand,
+} from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Card,
+ Checkbox,
+ FormControl,
+ FormHelperText,
+ IconButton,
+ List,
+ ListItem,
+ ListItemContent,
+ ListItemDecorator,
+ Typography,
+} from '@mui/joy'
+import { useEffect, useState } from 'react'
+
+const DEFAULT_SIDEPANEL_CONFIG = [
+ {
+ id: 'welcome',
+ name: 'Welcome Card',
+ description: 'Shows greeting and quick stats',
+ iconName: 'WavingHand',
+ enabled: true,
+ order: 0,
+ },
+ {
+ id: 'assignees',
+ name: 'Tasks by Assignee',
+ description: 'Groups tasks by who they are assigned to',
+ iconName: 'Person',
+ enabled: true,
+ order: 1,
+ },
+ {
+ id: 'calendar',
+ name: 'Calendar View',
+ description: 'Shows tasks in a calendar format',
+ iconName: 'CalendarMonth',
+ enabled: true,
+ order: 2,
+ },
+ {
+ id: 'activities',
+ name: 'Recent Activities',
+ description: 'Shows recent task completions and activities',
+ iconName: 'History',
+ enabled: true,
+ order: 3,
+ },
+]
+
+const SidepanelSettings = () => {
+ const [config, setConfig] = useState(DEFAULT_SIDEPANEL_CONFIG)
+
+ const getIcon = iconName => {
+ switch (iconName) {
+ case 'WavingHand':
+ return
+ case 'Person':
+ return
+ case 'CalendarMonth':
+ return
+ case 'History':
+ return
+ default:
+ return
+ }
+ }
+
+ useEffect(() => {
+ const saved = localStorage.getItem('sidepanelConfig')
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved)
+ setConfig(parsed)
+ } catch (error) {
+ console.error('Error parsing sidepanel config:', error)
+ }
+ }
+ }, [])
+
+ const saveConfig = newConfig => {
+ setConfig(newConfig)
+ localStorage.setItem('sidepanelConfig', JSON.stringify(newConfig))
+ window.dispatchEvent(new Event('sidepanelConfigChanged'))
+ }
+
+ const handleToggleEnabled = (id, enabled) => {
+ const newConfig = config.map(item =>
+ item.id === id ? { ...item, enabled } : item,
+ )
+ saveConfig(newConfig)
+ }
+
+ const handleDragEnd = result => {
+ if (!result.destination) return
+
+ const newConfig = Array.from(config)
+ const [reorderedItem] = newConfig.splice(result.source.index, 1)
+ newConfig.splice(result.destination.index, 0, reorderedItem)
+
+ const updatedConfig = newConfig.map((item, index) => ({
+ ...item,
+ order: index,
+ }))
+
+ saveConfig(updatedConfig)
+ }
+
+ const resetToDefaults = () => {
+ saveConfig(DEFAULT_SIDEPANEL_CONFIG)
+ }
+
+ return (
+
+
+ Sidepanel Settings
+
+
+ Customize which cards appear in the sidepanel and their order. Drag and
+ drop to reorder, or toggle visibility for each card.
+
+
+
+
+ {provided => (
+
+ {config.map((item, index) => (
+
+ {(provided, snapshot) => (
+
+
+
+
+
+
+
+
+
+ {getIcon(item.iconName)}
+
+
+
+
+
+ {item.name}
+
+
+ - {item.description}
+
+
+
+
+
+
+ handleToggleEnabled(item.id, e.target.checked)
+ }
+ overlay
+ variant='plain'
+ size='lg'
+ checkedIcon={ }
+ uncheckedIcon={ }
+ />
+
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+
+
+ Reset to Defaults
+
+
+ This will restore all cards to their default visibility and order.
+
+
+
+ )
+}
+
+export default SidepanelSettings
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index ea257f2..fc2e741 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -25,6 +25,7 @@ import {
import { useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { version } from '../../../package.json'
+import UserProfileAvatar from '../../components/UserProfileAvatar'
import ThemeToggleButton from '../Settings/ThemeToggleButton'
import NavBarLink from './NavBarLink'
const links = [
@@ -113,9 +114,9 @@ const NavBar = () => {
return (
{
)}
- {
- navigate('/chores')
- }}
- >
-
-
- Done
-
- tick✓
-
-
-
+
+
+
+
{
sx={{
'& .MuiDrawer-content': {
position: 'fixed',
- pt: 'calc(env(safe-area-inset-top, 0px))',
+ // pt: 'calc(var(--safe-area-inset-top, 0px))',
left: 0,
- pb: 'calc(env(safe-area-inset-bottom, 0px))',
+ // pb: 'calc(var(--safe-area-inset-bottom, 0px))',
// height:
- // 'calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))',
+ // 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
overflow: 'auto',
zIndex: Z_INDEX.DRAWER,
},
@@ -254,7 +228,7 @@ const NavBar = () => {
p: 1,
color: 'text.tertiary',
textAlign: 'center',
- mb: 'calc(env(safe-area-inset-bottom, 0px) + 45px)',
+ mb: 'calc(var(--safe-area-inset-bottom, 0px) )',
// mb: -2,
}}
>