Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/hooks/useGeolocation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect, useState } from 'react'

const GEOLOCATION_KEY = 'userGeolocation'
const GEOLOCATION_MANUAL_KEY = 'userGeolocationManual'

export const useGeolocation = () => {
const [coords, setCoords] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [isManualOverride, setIsManualOverride] = useState(true)

// Load from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(GEOLOCATION_KEY)
const isManual = localStorage.getItem(GEOLOCATION_MANUAL_KEY) === 'true'

if (stored) {
try {
setCoords(JSON.parse(stored))
setIsManualOverride(isManual)
setLoading(false)
return
} catch (e) {
console.error('Failed to parse stored geolocation:', e)
}
}

// Auto-detect geolocation
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
position => {
const { latitude, longitude } = position.coords
const newCoords = { latitude, longitude }
setCoords(newCoords)
localStorage.setItem(GEOLOCATION_KEY, JSON.stringify(newCoords))
localStorage.setItem(GEOLOCATION_MANUAL_KEY, 'false')
setLoading(false)
},
err => {
console.warn('Geolocation error:', err)
setError(err.message)
// Use default (New York) if geolocation fails
const defaultCoords = { latitude: 40.7128, longitude: -74.006 }
setCoords(defaultCoords)
localStorage.setItem(GEOLOCATION_KEY, JSON.stringify(defaultCoords))
setLoading(false)
},
{
timeout: 5000,
enableHighAccuracy: false,
},
)
} else {
setError('Geolocation not supported')
const defaultCoords = { latitude: 40.7128, longitude: -74.006 }
setCoords(defaultCoords)
setLoading(false)
}
}, [])

const setManualLocation = (latitude, longitude) => {
const newCoords = { latitude, longitude }
setCoords(newCoords)
setIsManualOverride(true)
localStorage.setItem(GEOLOCATION_KEY, JSON.stringify(newCoords))
localStorage.setItem(GEOLOCATION_MANUAL_KEY, 'true')
}

return {
coords,
loading,
error,
isManualOverride,
setManualLocation,
}
}
84 changes: 84 additions & 0 deletions src/hooks/useWeather.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useQuery } from '@tanstack/react-query'

const OPEN_METEO_API = 'https://api.open-meteo.com/v1'

// Map WMO weather codes to icons and descriptions
const weatherCodeMap = {
0: { icon: '☀️', desc: 'Clear' },
1: { icon: '🌤️', desc: 'Mostly Clear' },
2: { icon: '⛅', desc: 'Partly Cloudy' },
3: { icon: '☁️', desc: 'Cloudy' },
45: { icon: '🌫️', desc: 'Foggy' },
48: { icon: '🌫️', desc: 'Foggy' },
51: { icon: '🌦️', desc: 'Light Drizzle' },
53: { icon: '🌦️', desc: 'Drizzle' },
55: { icon: '🌧️', desc: 'Heavy Drizzle' },
61: { icon: '🌧️', desc: 'Light Rain' },
63: { icon: '🌧️', desc: 'Rain' },
65: { icon: '⛈️', desc: 'Heavy Rain' },
71: { icon: '❄️', desc: 'Light Snow' },
73: { icon: '❄️', desc: 'Snow' },
75: { icon: '❄️', desc: 'Heavy Snow' },
77: { icon: '🌨️', desc: 'Snow Grains' },
80: { icon: '🌧️', desc: 'Light Showers' },
81: { icon: '🌧️', desc: 'Showers' },
82: { icon: '⛈️', desc: 'Heavy Showers' },
85: { icon: '🌨️', desc: 'Light Snow Showers' },
86: { icon: '🌨️', desc: 'Snow Showers' },
95: { icon: '⛈️', desc: 'Thunderstorm' },
96: { icon: '⛈️', desc: 'Thunderstorm with Hail' },
99: { icon: '⛈️', desc: 'Thunderstorm with Hail' },
}

export const useWeather = (latitude, longitude, date) => {
return useQuery({
queryKey: ['weather', latitude, longitude, date?.toISOString()?.split('T')[0]],
enabled: !!latitude && !!longitude && !!date,
staleTime: 1000 * 60 * 60, // 1 hour
gcTime: 1000 * 60 * 60 * 24, // 24 hours
queryFn: async () => {
const dateStr = date.toISOString().split('T')[0]

const params = new URLSearchParams({
latitude,
longitude,
start_date: dateStr,
end_date: dateStr,
daily: 'weather_code,temperature_2m_max,temperature_2m_min',
temperature_unit: 'celsius',
timezone: 'auto',
})

const response = await fetch(
`${OPEN_METEO_API}/forecast?${params.toString()}`,
)

if (!response.ok) {
throw new Error('Failed to fetch weather data')
}

const data = await response.json()
const dailyData = data.daily

if (!dailyData || dailyData.weather_code[0] == null) {
return null
}

const weatherCode = dailyData.weather_code[0]
const weatherInfo = weatherCodeMap[weatherCode] || {
icon: '🌡️',
desc: 'Unknown',
}
const tempMax = dailyData.temperature_2m_max[0]
const tempMin = dailyData.temperature_2m_min[0]

return {
icon: weatherInfo.icon,
description: weatherInfo.desc,
tempMax: Math.round(tempMax),
tempMin: Math.round(tempMin),
code: weatherCode,
}
},
})
}
22 changes: 13 additions & 9 deletions src/views/Chores/CompactChoreCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import {
getPriorityColor,
getTextColorFromBackgroundColor,
} from '../../utils/Colors.jsx'
import AssigneeAvatarGroup, {
getChoreAssigneeNames,
} from '../components/AssigneeAvatarGroup'
import ChoreActionMenu from '../components/ChoreActionMenu'

const CompactChoreCard = ({
Expand Down Expand Up @@ -84,15 +87,7 @@ const CompactChoreCard = ({
parts.push(getRecurrentChipText(chore))

// Assignee
if (chore.assignedTo) {
const assignee = performers.find(
p => p.userId === chore.assignedTo,
)?.displayName
if (assignee) parts.push(assignee)
}
if (chore.assignedTo === null) {
parts.push('Anyone')
}
parts.push(getChoreAssigneeNames(chore, performers).join(', '))

// Points
if (chore.points > 0) {
Expand Down Expand Up @@ -124,6 +119,10 @@ const CompactChoreCard = ({
bgcolor: 'background.level1',
boxShadow: 'sm',
},
...(chore.frontendCompleted && {
opacity: 0.72,
bgcolor: 'success.softBg',
}),
'&::before': {
content: '""',
position: 'absolute',
Expand Down Expand Up @@ -373,6 +372,10 @@ const CompactChoreCard = ({
sx={{
fontWeight: 600,
fontSize: 14,
textDecoration: chore.frontendCompleted ? 'line-through' : 'none',
color: chore.frontendCompleted
? 'text.tertiary'
: 'text.primary',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
Expand Down Expand Up @@ -403,6 +406,7 @@ const CompactChoreCard = ({

{/* Line 2: Metadata */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
<AssigneeAvatarGroup chore={chore} performers={performers} size={20} />
{getFrequencyIcon(chore)}
<Typography
level='body-xs'
Expand Down
36 changes: 27 additions & 9 deletions src/views/Chores/MyChores.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Add,
Bolt,
CalendarMonth,
CalendarViewWeek,
CancelRounded,
CheckBox,
CheckBoxOutlineBlank,
Expand Down Expand Up @@ -44,6 +45,7 @@ import { useMediaQuery } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import Logo from '../../Logo'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import {
ChoreFilters,
Expand All @@ -55,6 +57,7 @@ import { getSafeBottom } from '../../utils/SafeAreaUtils.js'
import TaskInput from '../components/AddTaskModal'
import CalendarDual from '../components/CalendarDual'
import CalendarMonthly from '../components/CalendarMonthly.jsx'
import CalendarWeekly from '../components/CalendarWeekly.jsx'
import ProjectSelector from '../components/ProjectSelector'
import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder'
import { useProjects } from '../Projects/ProjectQueries.js'
Expand Down Expand Up @@ -687,14 +690,14 @@ const MyChores = () => {
}

const toggleViewMode = () => {
const modes = ['default', 'compact', 'calendar']
const modes = ['default', 'compact', 'weekly', 'calendar']
const currentIndex = modes.indexOf(viewMode)
const nextIndex = (currentIndex + 1) % modes.length
const newMode = modes[nextIndex]
setViewMode(newMode)
localStorage.setItem('choreCardViewMode', newMode)

if (newMode !== 'calendar') {
if (newMode !== 'calendar' && newMode !== 'weekly') {
setSelectedCalendarDate(null)
}
}
Expand Down Expand Up @@ -817,6 +820,8 @@ const MyChores = () => {
setSearchFilter('All')
}

const isCalendarView = viewMode === 'calendar' || viewMode === 'weekly'

// Show error state when API is unreachable
if (choresError || membersError) {
return (
Expand Down Expand Up @@ -962,13 +967,17 @@ const MyChores = () => {
viewMode === 'default'
? 'Switch to Compact View'
: viewMode === 'compact'
? 'Switch to Calendar View'
: 'Switch to Card View'
? 'Switch to Weekly View'
: viewMode === 'weekly'
? 'Switch to Monthly View'
: 'Switch to Card View'
}
>
{viewMode === 'default' ? (
<ViewAgenda />
) : viewMode === 'compact' ? (
<CalendarViewWeek />
) : viewMode === 'weekly' ? (
<CalendarMonth />
) : (
<ViewModule />
Expand Down Expand Up @@ -1256,7 +1265,7 @@ const MyChores = () => {
? getFilteredChores.length === 0
: projectFilteredChores.length === 0) &&
// only if not in calendar view:
viewMode !== 'calendar' && (
!isCalendarView && (
<Box
sx={{
display: 'flex',
Expand Down Expand Up @@ -1297,7 +1306,7 @@ const MyChores = () => {
</Box>
)}
{searchTerm?.length > 0 &&
viewMode !== 'calendar' && (
!isCalendarView && (
<ChoreListView
chores={getFilteredChores}
viewMode={viewMode}
Expand All @@ -1310,7 +1319,7 @@ const MyChores = () => {
toggleChoreSelection={toggleChoreSelection}
/>
)}
{viewMode === 'calendar' && (
{isCalendarView && (
<>
{/* Summary Chips when no date selected */}
{/* <Box
Expand Down Expand Up @@ -1429,7 +1438,16 @@ const MyChores = () => {
</Box> */}
{/* Calendar Monthly View */}
<Box sx={{ mb: 2 }}>
{isLargeScreen ? (
{viewMode === 'weekly' ? (
<CalendarWeekly
chores={getFilteredChores}
performers={membersData?.res}
selectedDate={selectedCalendarDate}
onDateChange={date => {
setSelectedCalendarDate(date)
}}
/>
) : isLargeScreen ? (
<CalendarDual
chores={getFilteredChores}
onDateChange={date => {
Expand Down Expand Up @@ -1493,7 +1511,7 @@ const MyChores = () => {
</>
)}
{searchTerm.length === 0 &&
viewMode !== 'calendar' && (
!isCalendarView && (
<AccordionGroup transition='0.2s ease' disableDivider>
{choreSections.map((section, index) => {
if (section.content.length === 0) return null
Expand Down
Loading