From c5ad9dfacfb4cdba57dc559452f16a573c7bb224 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 4 Dec 2025 16:32:59 +0330 Subject: [PATCH 1/3] feat(widget): add YadkarWidget and merge todos/notes --- src/context/widget-visibility.context.tsx | 87 +++++++++++++++------- src/layouts/widgets/notes/notes.layout.tsx | 36 ++++----- src/layouts/widgets/todos/todos.tsx | 37 ++++----- src/layouts/widgets/yadkar/yadkar.tsx | 15 ++++ 4 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 src/layouts/widgets/yadkar/yadkar.tsx diff --git a/src/context/widget-visibility.context.tsx b/src/context/widget-visibility.context.tsx index 6d6f907f..98c24ea9 100644 --- a/src/context/widget-visibility.context.tsx +++ b/src/context/widget-visibility.context.tsx @@ -21,6 +21,7 @@ import { YouTubeLayout } from '@/layouts/widgets/youtube/youtube.layout' import { useAuth } from './auth.context' import { CurrencyProvider } from './currency.context' import { showToast } from '@/common/toast' +import { YadkarWidget } from '@/layouts/widgets/yadkar/yadkar' export enum WidgetKeys { comboWidget = 'comboWidget', @@ -34,6 +35,7 @@ export enum WidgetKeys { youtube = 'youtube', wigiPad = 'wigiPad', network = 'network', + yadKar = 'yadKar', } export interface WidgetItem { id: WidgetKeys @@ -58,6 +60,15 @@ export const widgetItems: WidgetItem[] = [ canToggle: true, popular: true, }, + { + id: WidgetKeys.yadKar, + emoji: '📒', + label: 'یادکار (وظایف و یادداشت)', + order: 0, + node: , + canToggle: true, + isNew: true, + }, { id: WidgetKeys.tools, emoji: '🧰', @@ -66,14 +77,7 @@ export const widgetItems: WidgetItem[] = [ node: , canToggle: true, }, - { - id: WidgetKeys.todos, - emoji: '✅', - label: 'وظایف', - order: 2, - node: , - canToggle: true, - }, + { id: WidgetKeys.weather, emoji: '🌤️', @@ -117,12 +121,13 @@ export const widgetItems: WidgetItem[] = [ }, { - id: WidgetKeys.notes, - emoji: '📝', - label: 'یادداشت‌ها', - order: 7, - node: , + id: WidgetKeys.network, + emoji: '🌐', + label: 'شبکه', + order: 9, + node: , canToggle: true, + isNew: false, }, { id: WidgetKeys.youtube, @@ -135,13 +140,22 @@ export const widgetItems: WidgetItem[] = [ soon: true, }, { - id: WidgetKeys.network, - emoji: '🌐', - label: 'شبکه', - order: 9, - node: , - canToggle: true, - isNew: true, + id: WidgetKeys.todos, + emoji: '✅', + label: 'وظایف', + order: 2, + node: , + canToggle: false, + disabled: true, + }, + { + id: WidgetKeys.notes, + emoji: '📝', + label: 'یادداشت‌ها', + order: 7, + node: , + canToggle: false, + disabled: true, }, ] @@ -155,7 +169,7 @@ interface WidgetVisibilityContextType { const defaultVisibility: WidgetKeys[] = [ WidgetKeys.calendar, WidgetKeys.tools, - WidgetKeys.todos, + WidgetKeys.yadKar, WidgetKeys.comboWidget, ] export const MAX_VISIBLE_WIDGETS = 5 @@ -178,6 +192,17 @@ export function WidgetVisibilityProvider({ children }: { children: ReactNode }) useState>(getDefaultWidgetOrders) const firstRender = useRef(true) const { isAuthenticated } = useAuth() + + const saveActiveWidgets = () => { + const activeWidgets = widgetItems + .filter((item) => visibility.includes(item.id)) + .map((item) => ({ + ...item, + order: widgetOrders[item.id] ?? item.order, + })) + setToStorage('activeWidgets', activeWidgets) + } + useEffect(() => { async function loadSettings() { const storedVisibility = await getFromStorage('activeWidgets') @@ -185,6 +210,16 @@ export function WidgetVisibilityProvider({ children }: { children: ReactNode }) const visibilityIds = storedVisibility .filter((item) => widgetItems.some((w) => w.id === item.id)) .map((item: any) => item.id as WidgetKeys) + + if ( + visibilityIds.includes(WidgetKeys.todos) || + visibilityIds.includes(WidgetKeys.notes) + ) { + Analytics.event('yadkar_merged') + visibilityIds.push(WidgetKeys.yadKar) + saveActiveWidgets() + } + setVisibility(visibilityIds) const orders: Record = {} as Record< @@ -205,17 +240,13 @@ export function WidgetVisibilityProvider({ children }: { children: ReactNode }) loadSettings() }, []) + useEffect(() => { if (!firstRender.current) { - const activeWidgets = widgetItems - .filter((item) => visibility.includes(item.id)) - .map((item) => ({ - ...item, - order: widgetOrders[item.id] ?? item.order, - })) - setToStorage('activeWidgets', activeWidgets) + saveActiveWidgets() } }, [visibility, widgetOrders]) + const toggleWidget = (widgetId: WidgetKeys) => { setVisibility((prev) => { const isCurrentlyVisible = prev.includes(widgetId) diff --git a/src/layouts/widgets/notes/notes.layout.tsx b/src/layouts/widgets/notes/notes.layout.tsx index 043383e5..63f19eb1 100644 --- a/src/layouts/widgets/notes/notes.layout.tsx +++ b/src/layouts/widgets/notes/notes.layout.tsx @@ -1,7 +1,6 @@ import moment from 'jalali-moment' import { PiNotepad } from 'react-icons/pi' import Analytics from '@/analytics' -import { RequireAuth } from '@/components/auth/require-auth' import { useGeneralSetting } from '@/context/general-setting.context' import { NotesProvider, useNotes } from '@/context/notes.context' import { WidgetContainer } from '../widget-container' @@ -76,27 +75,30 @@ function NoteList() { ) } -function NotesHeader() { - return ( -
-

- دفترچه یادداشت -

- - -
- ) +interface Prop { + onChangeTab?: any } - -export function NotesLayout() { +export function NotesLayout({ onChangeTab }: Prop) { return (
- - - - +
+
+
onChangeTab()} + className="cursor-pointer hover:bg-primary/10 rounded-xl py-0.5 px-1" + > + وظایف +
+
+ یادداشت +
+
+ + +
+
diff --git a/src/layouts/widgets/todos/todos.tsx b/src/layouts/widgets/todos/todos.tsx index e15d61b4..fac769e4 100644 --- a/src/layouts/widgets/todos/todos.tsx +++ b/src/layouts/widgets/todos/todos.tsx @@ -31,8 +31,11 @@ import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' import { useIsMutating } from '@tanstack/react-query' import { IconLoading } from '@/components/loading/icon-loading' -export function TodosLayout() { - const { selectedDate, isToday } = useDate() +interface Prop { + onChangeTab?: any +} +export function TodosLayout({ onChangeTab }: Prop) { + const { selectedDate } = useDate() const { isAuthenticated } = useAuth() const { addTodo, todos, updateOptions, todoOptions, reorderTodos } = useTodoStore() const { blurMode } = useGeneralSetting() @@ -112,29 +115,27 @@ export function TodosLayout() { const updateMutating = useIsMutating({ mutationKey: ['updateTodo'] }) const addMutating = useIsMutating({ mutationKey: ['addTodo'] }) const isUpdating = updateMutating > 0 || addMutating > 0 - + console.log('todo') return (
-

onChangeTab?.()} > - وظایف - - {todoOptions.viewMode === TodoViewType.Monthly - ? `${selectedDate.format('jMMMM')} ماه` - : todoOptions.viewMode === TodoViewType.All - ? '' - : isToday(selectedDate) - ? ' امروز' - : ` ${selectedDate.format('jD jMMMM')}`} - +
+ وظایـف +
+
onChangeTab()} + > + یادداشت +
{isUpdating && } -

+
diff --git a/src/layouts/widgets/yadkar/yadkar.tsx b/src/layouts/widgets/yadkar/yadkar.tsx new file mode 100644 index 00000000..5b8b9cff --- /dev/null +++ b/src/layouts/widgets/yadkar/yadkar.tsx @@ -0,0 +1,15 @@ +import Analytics from '@/analytics' +import { NotesLayout } from '../notes/notes.layout' +import { TodosLayout } from '../todos/todos' + +export function YadkarWidget() { + const [tab, setTab] = useState<'todo' | 'note'>('todo') + + useEffect(() => { + Analytics.event('yadkar_change_tab') + }, [tab]) + + if (tab === 'todo') { + return setTab('note')} /> + } else return setTab('todo')} /> +} From 21ce2fa8ff04b8c2eb3531bae5c477b9fdaa1bff Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 4 Dec 2025 17:37:39 +0330 Subject: [PATCH 2/3] refactor(notes): migrate notes API to react-query hooks and improve UX --- src/context/notes.context.tsx | 56 +++++++++---------- .../notes/components/note-navigation.tsx | 22 +++++++- src/layouts/widgets/notes/notes.layout.tsx | 11 ++-- src/services/hooks/note/delete-note.hook.ts | 14 +++++ src/services/hooks/note/get-notes.hook.ts | 19 +++++++ src/services/hooks/note/note.interface.ts | 20 +++++++ src/services/hooks/note/upsert-note.hook.ts | 18 ++++++ src/services/note/note-api.ts | 41 -------------- 8 files changed, 120 insertions(+), 81 deletions(-) create mode 100644 src/services/hooks/note/delete-note.hook.ts create mode 100644 src/services/hooks/note/get-notes.hook.ts create mode 100644 src/services/hooks/note/note.interface.ts create mode 100644 src/services/hooks/note/upsert-note.hook.ts delete mode 100644 src/services/note/note-api.ts diff --git a/src/context/notes.context.tsx b/src/context/notes.context.tsx index 712d52aa..d7cf06e1 100644 --- a/src/context/notes.context.tsx +++ b/src/context/notes.context.tsx @@ -9,16 +9,14 @@ import { } from 'react' import Analytics from '@/analytics' import { getFromStorage, setToStorage } from '@/common/storage' -import { sleep } from '@/common/utils/timeout' import { safeAwait } from '@/services/api' -import { - deleteNote, - type FetchedNote, - getNotes, - upsertNote, -} from '@/services/note/note-api' import { translateError } from '@/utils/translate-error' import { showToast } from '@/common/toast' +import { useGetNotes } from '@/services/hooks/note/get-notes.hook' +import { useAuth } from './auth.context' +import { useRemoveNote } from '@/services/hooks/note/delete-note.hook' +import { useUpsertNote } from '@/services/hooks/note/upsert-note.hook' +import type { FetchedNote } from '@/services/hooks/note/note.interface' export interface Note { id: string @@ -42,11 +40,15 @@ interface NotesContextType { const NotesContext = createContext(undefined) export function NotesProvider({ children }: { children: ReactNode }) { + const { isAuthenticated } = useAuth() const [notes, setNotes] = useState([]) const [activeNoteId, setActiveNoteId] = useState(null) const [isSaving, setIsSaving] = useState(false) const [isCreatingNote, setIsCreatingNote] = useState(false) const saveTimeoutRef = useRef(null) + const { data: fetchedNotes, dataUpdatedAt, refetch } = useGetNotes(isAuthenticated) + const { mutateAsync: removeNoteAsync } = useRemoveNote() + const { mutateAsync: upsertNoteAsync } = useUpsertNote() useEffect(() => { async function loadNotes() { @@ -54,21 +56,17 @@ export function NotesProvider({ children }: { children: ReactNode }) { if (storedNotes && Array.isArray(storedNotes) && storedNotes.length > 0) { setNotes(storedNotes) } - - await sleep(1000) - setIsSaving(true) - const [error, fetchedNotes] = await safeAwait( - getNotes() - ) - setIsSaving(false) - if (!error) { - setNotes(fetchedNotes) - } } loadNotes() }, []) + useEffect(() => { + if (fetchedNotes.length) { + setNotes(fetchedNotes) + } + }, [dataUpdatedAt]) + const addNote = async () => { if (isCreatingNote) return @@ -83,12 +81,13 @@ export function NotesProvider({ children }: { children: ReactNode }) { } const [er, createdNote] = await safeAwait( - upsertNote(newNote) + upsertNoteAsync(newNote) ) setIsCreatingNote(false) if (er) { + showToast(translateError(er) as any, 'error') return } @@ -111,7 +110,7 @@ export function NotesProvider({ children }: { children: ReactNode }) { saveTimeoutRef.current = setTimeout(async () => { Analytics.event('update_notes') const [error, updatedNote] = await safeAwait( - upsertNote({ + upsertNoteAsync({ title: updates.title || null, body: updates.body || null, id, @@ -137,20 +136,17 @@ export function NotesProvider({ children }: { children: ReactNode }) { } const onDeleteNote = async (id: string) => { - const existingNote = notes.findIndex((note) => note.id === id) - if (existingNote !== -1) { - setIsSaving(true) - const updatedNotes = [...notes] - updatedNotes.splice(existingNote, 1) - setNotes(updatedNotes) - setActiveNoteId(null) - - await safeAwait(deleteNote(id)) - - await setToStorage('notes_data', updatedNotes) + setIsSaving(true) + const [err, _] = await safeAwait(removeNoteAsync(id)) + if (err) { setIsSaving(false) + return showToast(translateError(err) as any, 'error') } + Analytics.event('delete_notes') + setActiveNoteId(null) + refetch() + setIsSaving(false) } return ( diff --git a/src/layouts/widgets/notes/components/note-navigation.tsx b/src/layouts/widgets/notes/components/note-navigation.tsx index 5b671008..6f8fb1f9 100644 --- a/src/layouts/widgets/notes/components/note-navigation.tsx +++ b/src/layouts/widgets/notes/components/note-navigation.tsx @@ -4,8 +4,13 @@ import { FiChevronLeft, FiLoader, FiTrash2 } from 'react-icons/fi' import { Button } from '@/components/button/button' import Tooltip from '@/components/toolTip' import { useNotes } from '@/context/notes.context' +import { useAuth } from '@/context/auth.context' +import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' +import Analytics from '@/analytics' export function NoteNavigation() { + const { isAuthenticated } = useAuth() + const [isOpen, setIsOpen] = useState(false) const { notes, activeNoteId, @@ -21,15 +26,24 @@ export function NoteNavigation() { return notes.findIndex((note) => note.id === activeNoteId) }, [notes, activeNoteId]) - function onBackToList() { + const onBackToList = () => { setActiveNoteId(null) } - function onDelete() { + const onDelete = () => { setShowDeleteConfirm(false) deleteNote(activeNoteId as string) } + const onAdd = () => { + if (!isAuthenticated) { + setIsOpen(true) + Analytics.event('note_open_required_auth_modal') + return + } + addNote() + } + return (
@@ -74,7 +88,7 @@ export function NoteNavigation() { ) : (
+ + setIsOpen(false)} />
) } diff --git a/src/layouts/widgets/notes/notes.layout.tsx b/src/layouts/widgets/notes/notes.layout.tsx index 63f19eb1..16e3a985 100644 --- a/src/layouts/widgets/notes/notes.layout.tsx +++ b/src/layouts/widgets/notes/notes.layout.tsx @@ -8,7 +8,7 @@ import { NoteEditor } from './components/note-editor' import { NoteNavigation } from './components/note-navigation' function NotesContent() { - const { notes, activeNoteId, addNote, updateNote } = useNotes() + const { notes, activeNoteId, updateNote } = useNotes() const { blurMode } = useGeneralSetting() const activeNote = notes.find((note) => note.id === activeNoteId) @@ -20,12 +20,9 @@ function NotesContent() { >

یادداشتی پیدا نشد

- + + منتظر چی هستی؟ شروع کن به نوشتن! +
) } diff --git a/src/services/hooks/note/delete-note.hook.ts b/src/services/hooks/note/delete-note.hook.ts new file mode 100644 index 00000000..30765a16 --- /dev/null +++ b/src/services/hooks/note/delete-note.hook.ts @@ -0,0 +1,14 @@ +import { useMutation } from '@tanstack/react-query' +import { getMainClient } from '../../api' + +export const useRemoveNote = () => { + return useMutation({ + mutationKey: ['removeNote'], + mutationFn: (id: string) => deleteNote(id), + }) +} + +export async function deleteNote(id: string) { + const api = await getMainClient() + await api.delete(`/notes/${id}`) +} diff --git a/src/services/hooks/note/get-notes.hook.ts b/src/services/hooks/note/get-notes.hook.ts new file mode 100644 index 00000000..bdf53567 --- /dev/null +++ b/src/services/hooks/note/get-notes.hook.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query' +import { getMainClient } from '../../api' +import type { FetchedNote, GetNotesResponse } from './note.interface' + +export async function getNotes(): Promise { + const api = await getMainClient() + const response = await api.get('/notes') + return response.data.notes +} + +export const useGetNotes = (enabled: boolean) => { + return useQuery({ + queryKey: ['getNotes'], + queryFn: async () => getNotes(), + retry: 0, + enabled, + initialData: [], + }) +} diff --git a/src/services/hooks/note/note.interface.ts b/src/services/hooks/note/note.interface.ts new file mode 100644 index 00000000..71acbf11 --- /dev/null +++ b/src/services/hooks/note/note.interface.ts @@ -0,0 +1,20 @@ +export interface NoteCreateInput { + title?: string | null + body?: string | null + id?: string +} +export interface FetchedNote { + id: string + + title: string + body: string + + createdAt: number + updatedAt: number +} + +export interface GetNotesResponse { + notes: FetchedNote[] + total: number + totalPages: number +} diff --git a/src/services/hooks/note/upsert-note.hook.ts b/src/services/hooks/note/upsert-note.hook.ts new file mode 100644 index 00000000..0a217062 --- /dev/null +++ b/src/services/hooks/note/upsert-note.hook.ts @@ -0,0 +1,18 @@ +import { useMutation } from '@tanstack/react-query' +import { getMainClient } from '../../api' +import type { FetchedNote, NoteCreateInput } from './note.interface' + +export const useUpsertNote = () => { + return useMutation({ + mutationKey: ['upsertNote'], + mutationFn: (input: NoteCreateInput) => upsertNote(input), + }) +} + +export async function upsertNote(input: NoteCreateInput): Promise { + const api = await getMainClient() + + const response = await api.post('/notes', input) + + return response.data +} diff --git a/src/services/note/note-api.ts b/src/services/note/note-api.ts deleted file mode 100644 index 5e49a3f2..00000000 --- a/src/services/note/note-api.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getMainClient } from '../api' - -interface Input { - title?: string | null - body?: string | null - id?: string -} -export interface FetchedNote { - id: string - - title: string - body: string - - createdAt: number - updatedAt: number -} - -export interface GetNotesResponse { - notes: FetchedNote[] - total: number - totalPages: number -} - -export async function upsertNote(input: Input): Promise { - const api = await getMainClient() - - const response = await api.post('/notes', input) - - return response.data -} - -export async function getNotes(): Promise { - const api = await getMainClient() - const response = await api.get('/notes') - return response.data.notes -} - -export async function deleteNote(id: string) { - const api = await getMainClient() - await api.delete(`/notes/${id}`) -} From f23168176686d4862d42fe37dee75567ac34dd97 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 4 Dec 2025 19:03:50 +0330 Subject: [PATCH 3/3] feat(notes): add priority system with UI and sync improvements --- src/common/constant/priority_options.ts | 20 +++++++ src/context/notes.context.tsx | 54 ++++++++--------- .../widgets/notes/components/note-editor.tsx | 59 ++++++++++++++++++- .../widgets/notes/components/note-item.tsx | 27 +++++++++ .../notes/components/note-navigation.tsx | 11 ++-- src/layouts/widgets/notes/notes.layout.tsx | 15 +---- src/layouts/widgets/todos/todos.tsx | 1 - src/services/hooks/note/get-notes.hook.ts | 1 - src/services/hooks/note/note.interface.ts | 3 +- 9 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 src/common/constant/priority_options.ts create mode 100644 src/layouts/widgets/notes/components/note-item.tsx diff --git a/src/common/constant/priority_options.ts b/src/common/constant/priority_options.ts new file mode 100644 index 00000000..0b3c50fe --- /dev/null +++ b/src/common/constant/priority_options.ts @@ -0,0 +1,20 @@ +export const PRIORITY_OPTIONS = [ + { + value: 'low', + ariaLabel: 'اولویت کم', + bgColor: 'bg-green-500', + hoverBgColor: 'hover:bg-green-600', + }, + { + value: 'medium', + ariaLabel: 'اولویت متوسط', + bgColor: 'bg-yellow-400', + hoverBgColor: 'hover:bg-yellow-400', + }, + { + value: 'high', + ariaLabel: 'اولویت زیاد', + bgColor: 'bg-red-500', + hoverBgColor: 'hover:bg-red-500', + }, +] diff --git a/src/context/notes.context.tsx b/src/context/notes.context.tsx index d7cf06e1..204d8ea0 100644 --- a/src/context/notes.context.tsx +++ b/src/context/notes.context.tsx @@ -18,20 +18,12 @@ import { useRemoveNote } from '@/services/hooks/note/delete-note.hook' import { useUpsertNote } from '@/services/hooks/note/upsert-note.hook' import type { FetchedNote } from '@/services/hooks/note/note.interface' -export interface Note { - id: string - title: string - body: string - createdAt: number - updatedAt: number -} - interface NotesContextType { - notes: Note[] + notes: FetchedNote[] activeNoteId: string | null setActiveNoteId: (id: string | null) => void addNote: () => void - updateNote: (id: string, updates: Partial) => void + updateNote: (id: string, updates: Partial) => void deleteNote: (id: string) => void isSaving: boolean isCreatingNote: boolean @@ -41,19 +33,20 @@ const NotesContext = createContext(undefined) export function NotesProvider({ children }: { children: ReactNode }) { const { isAuthenticated } = useAuth() - const [notes, setNotes] = useState([]) + const [notes, setNotes] = useState([]) const [activeNoteId, setActiveNoteId] = useState(null) const [isSaving, setIsSaving] = useState(false) const [isCreatingNote, setIsCreatingNote] = useState(false) const saveTimeoutRef = useRef(null) - const { data: fetchedNotes, dataUpdatedAt, refetch } = useGetNotes(isAuthenticated) + + const { data: fetchedNotes, refetch, dataUpdatedAt } = useGetNotes(isAuthenticated) const { mutateAsync: removeNoteAsync } = useRemoveNote() const { mutateAsync: upsertNoteAsync } = useUpsertNote() useEffect(() => { async function loadNotes() { const storedNotes = await getFromStorage('notes_data') - if (storedNotes && Array.isArray(storedNotes) && storedNotes.length > 0) { + if (storedNotes && storedNotes.length > 0) { setNotes(storedNotes) } } @@ -62,8 +55,9 @@ export function NotesProvider({ children }: { children: ReactNode }) { }, []) useEffect(() => { + console.log('fetchedNotes') if (fetchedNotes.length) { - setNotes(fetchedNotes) + sync(fetchedNotes, true) } }, [dataUpdatedAt]) @@ -72,7 +66,7 @@ export function NotesProvider({ children }: { children: ReactNode }) { setIsCreatingNote(true) - const newNote: Note = { + const newNote: FetchedNote = { id: '', title: '', body: '', @@ -91,16 +85,12 @@ export function NotesProvider({ children }: { children: ReactNode }) { return } - setNotes((prevNotes) => { - const updatedNotes = [createdNote, ...prevNotes] - setToStorage('notes_data', updatedNotes) - return updatedNotes - }) + sync([createdNote, ...notes], true) setActiveNoteId(createdNote.id) Analytics.event('add_notes') } - const updateNote = (id: string, updates: Partial) => { + const updateNote = (id: string, updates: Partial) => { setIsSaving(true) if (saveTimeoutRef.current) { @@ -114,6 +104,7 @@ export function NotesProvider({ children }: { children: ReactNode }) { title: updates.title || null, body: updates.body || null, id, + priority: updates.priority, }) ) setIsSaving(false) @@ -126,13 +117,11 @@ export function NotesProvider({ children }: { children: ReactNode }) { return showToast(`${key}: ${translatedError[key]}`, 'error') } - // update notes - const updatedNotes = notes.map((note) => - note.id === id ? updatedNote : note - ) - - await setToStorage('notes_data', updatedNotes) - }, 2500) + let noteIndex = notes.findIndex((n) => n.id === id) + if (noteIndex === -1) return showToast('یادداشت پیدا نشد', 'error') + notes[noteIndex] = updatedNote + sync(notes, true) + }, 400) } const onDeleteNote = async (id: string) => { @@ -143,12 +132,19 @@ export function NotesProvider({ children }: { children: ReactNode }) { return showToast(translateError(err) as any, 'error') } + refetch() Analytics.event('delete_notes') setActiveNoteId(null) - refetch() setIsSaving(false) } + const sync = (data: FetchedNote[], syncLocal: boolean) => { + setNotes(data) + if (syncLocal) { + setToStorage('notes_data', notes) + } + } + return ( ) => void + note: FetchedNote + onUpdateNote: (id: string, updates: Partial) => void } const EDITOR_DEBOUNCE_TIME = 500 //0.5s @@ -11,6 +14,7 @@ const EDITOR_DEBOUNCE_TIME = 500 //0.5s export function NoteEditor({ note, onUpdateNote }: NoteEditorProps) { const titleRef = useRef(null) const bodyRef = useRef(null) + const [priority, setPriority] = useState(note.priority) const [currentTitle, setCurrentTitle] = useState(note.title) const [currentBody, setCurrentBody] = useState(note.body) @@ -63,12 +67,22 @@ export function NoteEditor({ note, onUpdateNote }: NoteEditorProps) { updateTimeoutRef.current = setTimeout(() => { onUpdateNote(note.id, { + ...note, title: note.title, body: note.body, }) }, EDITOR_DEBOUNCE_TIME) } + const onPriority = (value: string) => { + setPriority(value as any) + onUpdateNote(note.id, { + priority: value as any, + body: note.body, + title: note.title, + }) + } + return (
+ +
+
+ {PRIORITY_OPTIONS.map((p) => ( + onPriority(p.value)} + option={p} + /> + ))} +
+
+
) } + +const PriorityButton = ({ + option, + isSelected, + onClick, +}: { + option: (typeof PRIORITY_OPTIONS)[0] + isSelected: boolean + onClick: () => void +}) => ( + + + +) diff --git a/src/layouts/widgets/notes/components/note-item.tsx b/src/layouts/widgets/notes/components/note-item.tsx new file mode 100644 index 00000000..099109b7 --- /dev/null +++ b/src/layouts/widgets/notes/components/note-item.tsx @@ -0,0 +1,27 @@ +import { PRIORITY_OPTIONS } from '@/common/constant/priority_options' +import type { FetchedNote } from '@/services/hooks/note/note.interface' +import moment from 'jalali-moment' + +interface Prop { + note: FetchedNote + handleNoteClick: any +} +export function NoteItem({ note, handleNoteClick }: Prop) { + const p = PRIORITY_OPTIONS.find((p) => p.value === note.priority) + return ( +
handleNoteClick(note.id)} + > +
+ + + {note.title || 'بدون عنوان'} + + + {moment(note.createdAt).locale('fa').format('jD jMMM YY')} + +
+ ) +} diff --git a/src/layouts/widgets/notes/components/note-navigation.tsx b/src/layouts/widgets/notes/components/note-navigation.tsx index 6f8fb1f9..da45e0ae 100644 --- a/src/layouts/widgets/notes/components/note-navigation.tsx +++ b/src/layouts/widgets/notes/components/note-navigation.tsx @@ -1,12 +1,13 @@ import { useMemo } from 'react' import { FaPlus } from 'react-icons/fa6' -import { FiChevronLeft, FiLoader, FiTrash2 } from 'react-icons/fi' +import { FiChevronLeft, FiTrash2 } from 'react-icons/fi' import { Button } from '@/components/button/button' import Tooltip from '@/components/toolTip' import { useNotes } from '@/context/notes.context' import { useAuth } from '@/context/auth.context' import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' import Analytics from '@/analytics' +import { IconLoading } from '@/components/loading/icon-loading' export function NoteNavigation() { const { isAuthenticated } = useAuth() @@ -47,11 +48,7 @@ export function NoteNavigation() { return (
- {isSaving && ( - - )} + {isSaving && } {activeNoteId ? ( <> {isCreatingNote ? ( - + ) : ( )} diff --git a/src/layouts/widgets/notes/notes.layout.tsx b/src/layouts/widgets/notes/notes.layout.tsx index 16e3a985..9edae650 100644 --- a/src/layouts/widgets/notes/notes.layout.tsx +++ b/src/layouts/widgets/notes/notes.layout.tsx @@ -1,4 +1,3 @@ -import moment from 'jalali-moment' import { PiNotepad } from 'react-icons/pi' import Analytics from '@/analytics' import { useGeneralSetting } from '@/context/general-setting.context' @@ -6,6 +5,7 @@ import { NotesProvider, useNotes } from '@/context/notes.context' import { WidgetContainer } from '../widget-container' import { NoteEditor } from './components/note-editor' import { NoteNavigation } from './components/note-navigation' +import { NoteItem } from './components/note-item' function NotesContent() { const { notes, activeNoteId, updateNote } = useNotes() @@ -55,18 +55,7 @@ function NoteList() { className={`w-full overflow-y-auto hide-scrollbar h-96 flex flex-col gap-0.5 mt-4 ${blurMode ? 'blur-mode' : 'disabled-blur-mode'}`} > {notes.map((note) => ( -
handleNoteClick(note.id)} - > - - {note.title || 'بدون عنوان'} - - - {moment(note.createdAt).locale('fa').format('jD jMMM YY')} - -
+ ))}
) diff --git a/src/layouts/widgets/todos/todos.tsx b/src/layouts/widgets/todos/todos.tsx index fac769e4..be1eb493 100644 --- a/src/layouts/widgets/todos/todos.tsx +++ b/src/layouts/widgets/todos/todos.tsx @@ -115,7 +115,6 @@ export function TodosLayout({ onChangeTab }: Prop) { const updateMutating = useIsMutating({ mutationKey: ['updateTodo'] }) const addMutating = useIsMutating({ mutationKey: ['addTodo'] }) const isUpdating = updateMutating > 0 || addMutating > 0 - console.log('todo') return (
diff --git a/src/services/hooks/note/get-notes.hook.ts b/src/services/hooks/note/get-notes.hook.ts index bdf53567..79e9d224 100644 --- a/src/services/hooks/note/get-notes.hook.ts +++ b/src/services/hooks/note/get-notes.hook.ts @@ -12,7 +12,6 @@ export const useGetNotes = (enabled: boolean) => { return useQuery({ queryKey: ['getNotes'], queryFn: async () => getNotes(), - retry: 0, enabled, initialData: [], }) diff --git a/src/services/hooks/note/note.interface.ts b/src/services/hooks/note/note.interface.ts index 71acbf11..8d424972 100644 --- a/src/services/hooks/note/note.interface.ts +++ b/src/services/hooks/note/note.interface.ts @@ -2,13 +2,14 @@ export interface NoteCreateInput { title?: string | null body?: string | null id?: string + priority?: 'low' | 'medium' | 'high' } export interface FetchedNote { id: string title: string body: string - + priority?: 'low' | 'medium' | 'high' createdAt: number updatedAt: number }