Skip to content

Commit ccec275

Browse files
authored
Merge pull request #373 from widgetify-app/optimistic-todo-updates
Optimistic todo updates
2 parents 70258b9 + ef96ffa commit ccec275

20 files changed

Lines changed: 694 additions & 537 deletions

File tree

src/context/todo.context.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useUpdateTodo } from '@/services/hooks/todo/update-todo.hook'
1414
import { useGetTodos } from '@/services/hooks/todo/get-todos.hook'
1515
import type { FetchedTodo, Todo } from '@/services/hooks/todo/todo.interface'
1616
import { playAlarm } from '@/common/playAlarm'
17+
import { sleep } from '@/common/utils/timeout'
1718

1819
export enum TodoViewType {
1920
Day = 'day',
@@ -159,17 +160,21 @@ export function TodoProvider({ children }: { children: React.ReactNode }) {
159160
? Math.max(...sameDateTodos.map((t) => t.order || 0))
160161
: 0
161162

162-
const [err, _] = await safeAwait(
163-
addTodoAsync({
164-
text: input.text,
165-
completed: false,
166-
date: input.date,
167-
priority: input.priority || TodoPriority.Low,
168-
category: input.category || '',
169-
order: maxOrder + 1,
170-
description: input.notes || '',
171-
})
172-
)
163+
const item: any = {
164+
text: input.text,
165+
completed: false,
166+
date: input.date,
167+
priority: input.priority || TodoPriority.Low,
168+
category: input.category || '',
169+
order: maxOrder + 1,
170+
description: input.notes || '',
171+
}
172+
const id = `temp-${Date.now()}`
173+
old.unshift({ ...item, order: 0, id })
174+
setTodos(() => old)
175+
176+
await sleep(3000)
177+
const [err, _] = await safeAwait(addTodoAsync(item))
173178
if (err) {
174179
const content = translateError(err)
175180
if (typeof content === 'string') {
@@ -201,6 +206,20 @@ export function TodoProvider({ children }: { children: React.ReactNode }) {
201206
)
202207
}
203208
const isCompleted = !current.completed
209+
210+
setTodos((prev) => {
211+
if (!prev) return prev
212+
return prev.map((todo) => {
213+
if (todo.id === id || todo.onlineId === id) {
214+
return {
215+
...todo,
216+
completed: isCompleted,
217+
}
218+
}
219+
return todo
220+
})
221+
})
222+
204223
const [err, _] = await safeAwait(
205224
updateTodoAsync({
206225
id: onlineId,
@@ -214,9 +233,9 @@ export function TodoProvider({ children }: { children: React.ReactNode }) {
214233
showToast(translateError(err) as string, 'error')
215234
return
216235
}
217-
refetch()
218-
Analytics.event('todo_toggled')
236+
219237
if (isCompleted) playAlarm('done_todo')
238+
Analytics.event('todo_toggled')
220239
}
221240

222241
const updateTodo = async (id: string, updates: Partial<Omit<Todo, 'id'>>) => {

src/context/widget-visibility.context.tsx

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import CalendarLayout from '@/layouts/widgets/calendar/calendar'
1212
import { ComboWidget } from '@/layouts/widgets/comboWidget/combo-widget.layout'
1313
import { NetworkLayout } from '@/layouts/widgets/network/network.layout'
1414
import { NewsLayout } from '@/layouts/widgets/news/news.layout'
15-
import { NotesLayout } from '@/layouts/widgets/notes/notes.layout'
16-
import { TodosLayout } from '@/layouts/widgets/todos/todos'
1715
import { ToolsLayout } from '@/layouts/widgets/tools/tools.layout'
1816
import { WeatherLayout } from '@/layouts/widgets/weather/weather.layout'
1917
import { WigiArzLayout } from '@/layouts/widgets/wigiArz/wigi_arz.layout'
@@ -22,6 +20,7 @@ import { useAuth } from './auth.context'
2220
import { CurrencyProvider } from './currency.context'
2321
import { showToast } from '@/common/toast'
2422
import { YadkarWidget } from '@/layouts/widgets/yadkar/yadkar'
23+
import { TodoProvider } from './todo.context'
2524

2625
export enum WidgetKeys {
2726
comboWidget = 'comboWidget',
@@ -65,7 +64,11 @@ export const widgetItems: WidgetItem[] = [
6564
emoji: '📒',
6665
label: 'یادکار (وظایف و یادداشت)',
6766
order: 0,
68-
node: <YadkarWidget />,
67+
node: (
68+
<TodoProvider>
69+
<YadkarWidget />
70+
</TodoProvider>
71+
),
6972
canToggle: true,
7073
isNew: true,
7174
},
@@ -139,24 +142,6 @@ export const widgetItems: WidgetItem[] = [
139142
disabled: true,
140143
soon: true,
141144
},
142-
{
143-
id: WidgetKeys.todos,
144-
emoji: '✅',
145-
label: 'وظایف',
146-
order: 2,
147-
node: <TodosLayout />,
148-
canToggle: false,
149-
disabled: true,
150-
},
151-
{
152-
id: WidgetKeys.notes,
153-
emoji: '📝',
154-
label: 'یادداشت‌ها',
155-
order: 7,
156-
node: <NotesLayout />,
157-
canToggle: false,
158-
disabled: true,
159-
},
160145
]
161146

162147
interface WidgetVisibilityContextType {

src/layouts/widgetify-card/widgetify.layout.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { useAuth } from '@/context/auth.context'
55
import { useGeneralSetting } from '@/context/general-setting.context'
66
import { WidgetContainer } from '../widgets/widget-container'
77
import { NotificationCenter } from './notification-center/notification-center'
8-
import { TodoOverviewCard } from './overviewCards/todo-overviewCard'
98
import { Pet } from './pets/pet'
109
import { PetProvider } from './pets/pet.context'
1110
import Snowfall from 'react-snowfall'
@@ -26,7 +25,6 @@ export const WidgetifyLayout = () => {
2625
const newBlurMode = !blurMode
2726
updateSetting('blurMode', newBlurMode)
2827
}
29-
3028
return (
3129
<WidgetContainer className="overflow-hidden !h-72 !min-h-72 !max-h-72">
3230
<div className="relative w-full h-full">
@@ -60,7 +58,7 @@ export const WidgetifyLayout = () => {
6058
<div
6159
className={`flex flex-col gap-1 ${blurMode ? 'blur-mode' : 'disabled-blur-mode'}`}
6260
>
63-
<TodoOverviewCard />
61+
{/* <TodoOverviewCard /> */}
6462
{/* <GoogleOverviewCard /> */}
6563
</div>
6664
<NotificationCenter />

src/layouts/widgets/calendar/calendar.tsx

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,112 @@
1-
import type React from 'react'
1+
import { type ReactNode, useState } from 'react'
2+
import GoogleCalendar from '@/assets/google-calendar.png'
23
import { useDate } from '@/context/date.context'
34
import { WidgetContainer } from '../widget-container'
45
import { CalendarGrid } from './components/calendar-grid'
56
import { CalendarHeader } from './components/calendar-header'
6-
import { DaySummary } from './components/day-summary'
7+
import { GoogleCalendarView } from './components/google-calendar/google-calendar-view'
8+
import { FcCalendar } from 'react-icons/fc' // استفاده از لوگوی رنگی گوگل برای جلوه بهتر
9+
import Analytics from '@/analytics'
710

8-
const CalendarLayout: React.FC<any> = () => {
9-
const { currentDate, selectedDate, setCurrentDate, setSelectedDate, goToToday } =
10-
useDate()
11+
interface TabItem {
12+
label: string
13+
value: string
14+
icon?: ReactNode
15+
}
16+
17+
interface CalendarTabSelectorProps {
18+
tabs: TabItem[]
19+
activeTab: string
20+
setActiveTab: (tab: string) => void
21+
}
1122

23+
const CalendarTabSelector: React.FC<CalendarTabSelectorProps> = ({
24+
tabs,
25+
activeTab,
26+
setActiveTab,
27+
}) => {
1228
return (
13-
<WidgetContainer
14-
className={
15-
'flex flex-col overflow-hidden md:flex-1 w-full transition-all duration-300'
16-
}
17-
>
18-
<CalendarHeader
19-
currentDate={currentDate}
20-
setCurrentDate={setCurrentDate}
21-
selectedDate={selectedDate}
22-
goToToday={goToToday}
23-
/>
29+
<div className="p-1 mt-1 shrink-0">
30+
<div
31+
role="tablist"
32+
className="flex w-full gap-2 p-1.5 bg-base-200/50 rounded-2xl border border-base-300/30"
33+
>
34+
{tabs.map((tab) => (
35+
<button
36+
key={tab.value}
37+
onClick={() => setActiveTab(tab.value)}
38+
className={`flex-1 flex items-center justify-center gap-2 h-9 rounded-xl text-[11px] cursor-pointer font-bold transition-all duration-200 ${
39+
activeTab === tab.value
40+
? 'bg-base-100/30 text-primary-content border border-base-300 shadow-md scale-[1.02]'
41+
: 'text-base-content border border-transparent opacity-60 hover:opacity-100 hover:bg-base-100/50'
42+
}`}
43+
>
44+
{tab.icon}
45+
{tab.label}
46+
</button>
47+
))}
48+
</div>
49+
</div>
50+
)
51+
}
2452

25-
<CalendarGrid
26-
currentDate={currentDate}
27-
selectedDate={selectedDate}
28-
setSelectedDate={setSelectedDate}
53+
const tabs = [
54+
{
55+
label: 'تقویم',
56+
value: 'calendar',
57+
icon: <FcCalendar size={18} />,
58+
},
59+
{
60+
label: 'گوگل‌کلندر',
61+
value: 'google',
62+
icon: (
63+
<img
64+
src={GoogleCalendar}
65+
alt="Google Calendar"
66+
className="w-5 h-5 rounded-sm"
2967
/>
68+
),
69+
},
70+
]
71+
const CalendarLayout: React.FC = () => {
72+
const { currentDate, selectedDate, setCurrentDate, setSelectedDate, goToToday } =
73+
useDate()
74+
const [activeTab, setActiveTab] = useState<string>('calendar')
75+
76+
const onSetActiveTab = (tab: string) => {
77+
setActiveTab(tab)
78+
Analytics.event(`calendar_tab_switch_to_${tab}`)
79+
}
3080

31-
<div className="flex-1 mt-2">
32-
<DaySummary selectedDate={selectedDate} />
81+
return (
82+
<WidgetContainer className="flex flex-col w-full overflow-hidden transition-all duration-300 md:flex-1">
83+
<div className="flex flex-col flex-1 overflow-hidden">
84+
{activeTab === 'calendar' ? (
85+
<>
86+
<CalendarHeader
87+
currentDate={currentDate}
88+
selectedDate={selectedDate}
89+
setCurrentDate={setCurrentDate}
90+
goToToday={goToToday}
91+
/>
92+
<div className="h-full">
93+
<CalendarGrid
94+
currentDate={currentDate}
95+
selectedDate={selectedDate}
96+
setSelectedDate={setSelectedDate}
97+
/>
98+
</div>
99+
</>
100+
) : (
101+
<GoogleCalendarView />
102+
)}
33103
</div>
104+
105+
<CalendarTabSelector
106+
tabs={tabs}
107+
activeTab={activeTab}
108+
setActiveTab={onSetActiveTab}
109+
/>
34110
</WidgetContainer>
35111
)
36112
}

src/layouts/widgets/calendar/components/calendar-grid.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useAuth } from '@/context/auth.context'
22
import { useGeneralSetting } from '@/context/general-setting.context'
3-
import { useTodoStore } from '@/context/todo.context'
43
import { useGetEvents } from '@/services/hooks/date/getEvents.hook'
54
import type React from 'react'
65
import { useState } from 'react'
@@ -29,7 +28,12 @@ export const CalendarGrid: React.FC<CalendarGridProps> = ({
2928
const [clickedElement, setClickedElement] = useState<HTMLDivElement | null>(null)
3029

3130
const { data: events } = useGetEvents()
32-
const { todos } = useTodoStore()
31+
32+
const eventsForCalendar = events || {
33+
gregorianEvents: [],
34+
hijriEvents: [],
35+
shamsiEvents: [],
36+
}
3337

3438
const { data: calendarData, refetch } = useGetCalendarData(
3539
isAuthenticated,
@@ -76,11 +80,10 @@ export const CalendarGrid: React.FC<CalendarGridProps> = ({
7680
key={`day-${i}`}
7781
currentDate={currentDate}
7882
day={i + 1}
79-
events={events}
83+
events={eventsForCalendar}
8084
googleEvents={calendarData?.googleEvents || []}
8185
selectedDateStr={selectedDateStr}
8286
setSelectedDate={setSelectedDate}
83-
todos={todos}
8487
timezone={timezone.value}
8588
moods={calendarData?.moods ?? []}
8689
onClick={(element) => {
@@ -109,8 +112,7 @@ export const CalendarGrid: React.FC<CalendarGridProps> = ({
109112
triggerRef={{ current: clickedElement }}
110113
content={
111114
<CalendarDayDetails
112-
events={events}
113-
googleEvents={calendarData?.googleEvents || []}
115+
events={eventsForCalendar}
114116
moods={calendarData?.moods ?? []}
115117
onMoodChange={() => refetch()}
116118
/>

0 commit comments

Comments
 (0)