From ce333bc8e494f01cb4e54ad39b076455ea92142d Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 18 Jun 2026 03:04:35 +0330 Subject: [PATCH 01/17] fix --- src/styles/theme/light.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/styles/theme/light.css b/src/styles/theme/light.css index 2138db4e..55d5a88e 100644 --- a/src/styles/theme/light.css +++ b/src/styles/theme/light.css @@ -1,7 +1,6 @@ @plugin "daisyui/theme" { name: "light"; - default: true; --color-primary: var(--brand-primary); - --color-secondary: var(--brand-secondary); + --color-primary-focus: #4f46e5; --color-primary-content: rgba(var(--brand-primary-rgb), 0.9); } \ No newline at end of file From e7a3f315318fce354fbbc282234975a9d410f597 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Tue, 23 Jun 2026 12:08:43 +0330 Subject: [PATCH 02/17] fix light theme --- src/styles/theme/light.css | 1 + wxt.config.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/styles/theme/light.css b/src/styles/theme/light.css index 55d5a88e..658581d0 100644 --- a/src/styles/theme/light.css +++ b/src/styles/theme/light.css @@ -2,5 +2,6 @@ name: "light"; --color-primary: var(--brand-primary); --color-primary-focus: #4f46e5; + --color-secondary: var(--brand-secondary); --color-primary-content: rgba(var(--brand-primary-rgb), 0.9); } \ No newline at end of file diff --git a/wxt.config.ts b/wxt.config.ts index 1dc85c7a..e39a920c 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -48,7 +48,7 @@ export default defineConfig({ '@wxt-dev/module-react', ], manifest: { - version: '1.0.98', + version: '1.0.99', name: 'Widgetify', description: 'Transform your new tab into a smart dashboard with Widgetify! Get currency rates, crypto prices, weather & more.', From b5c7abc96d9eb1e1676b349f107151be2cf92172 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Tue, 23 Jun 2026 21:41:44 +0330 Subject: [PATCH 03/17] feat: implement habit tracking feature with forms and hooks --- src/common/constant/habit-options.ts | 53 +++ src/context/widget-visibility.context.tsx | 11 + .../habit/components/habit-form.modal.tsx | 316 ++++++++++++++++++ .../widgets/habit/components/habit.item.tsx | 142 ++++++++ src/layouts/widgets/habit/habits.layout.tsx | 190 +++++++++++ .../widgets/habit/utils/habit.utils.ts | 45 +++ src/services/hooks/habit/add-habit.hook.ts | 14 + .../hooks/habit/archive-habit.hook.ts | 13 + src/services/hooks/habit/get-habits.hook.ts | 34 ++ src/services/hooks/habit/habit.interface.ts | 68 ++++ .../hooks/habit/log-habit-progress.hook.ts | 20 ++ src/services/hooks/habit/update-habit.hook.ts | 14 + 12 files changed, 920 insertions(+) create mode 100644 src/common/constant/habit-options.ts create mode 100644 src/layouts/widgets/habit/components/habit-form.modal.tsx create mode 100644 src/layouts/widgets/habit/components/habit.item.tsx create mode 100644 src/layouts/widgets/habit/habits.layout.tsx create mode 100644 src/layouts/widgets/habit/utils/habit.utils.ts create mode 100644 src/services/hooks/habit/add-habit.hook.ts create mode 100644 src/services/hooks/habit/archive-habit.hook.ts create mode 100644 src/services/hooks/habit/get-habits.hook.ts create mode 100644 src/services/hooks/habit/habit.interface.ts create mode 100644 src/services/hooks/habit/log-habit-progress.hook.ts create mode 100644 src/services/hooks/habit/update-habit.hook.ts diff --git a/src/common/constant/habit-options.ts b/src/common/constant/habit-options.ts new file mode 100644 index 00000000..eec29617 --- /dev/null +++ b/src/common/constant/habit-options.ts @@ -0,0 +1,53 @@ +export const HABIT_UNIT_OPTIONS = [ + { value: 'TIMES', label: 'دفعه' }, + { value: 'MINUTES', label: 'دقیقه' }, + { value: 'HOURS', label: 'ساعت' }, + { value: 'PAGES', label: 'صفحه' }, + { value: 'GLASSES', label: 'لیوان' }, + { value: 'CUSTOM', label: 'دلخواه' }, +] + +export const HABIT_COMPARISON_OPTIONS = [ + { value: 'AT_LEAST', label: 'حداقل' }, + { value: 'AT_MOST', label: 'حداکثر' }, + { value: 'EXACT', label: 'دقیقا' }, +] + +export const HABIT_FREQUENCY_OPTIONS = [ + { value: 'DAILY', label: 'روزانه' }, + { value: 'WEEKLY', label: 'هفتگی' }, + { value: 'MONTHLY', label: 'ماهانه' }, +] + +export const HABIT_COLOR_PRESETS = [ + '#ef4444', + '#f97316', + '#f59e0b', + '#22c55e', + '#06b6d4', + '#3b82f6', + '#8b5cf6', + '#ec4899', +] + +export const HABIT_EMOJI_PRESETS = [ + '🎯', + '💧', + '📖', + '🏃', + '🧘', + '🥗', + '😴', + '✍️', + '💪', + '🚭', +] + +export const HABIT_UNIT_STEP: Record = { + TIMES: 1, + GLASSES: 1, + PAGES: 1, + MINUTES: 5, + HOURS: 1, + CUSTOM: 1, +} \ No newline at end of file diff --git a/src/context/widget-visibility.context.tsx b/src/context/widget-visibility.context.tsx index fa529681..18c425e0 100644 --- a/src/context/widget-visibility.context.tsx +++ b/src/context/widget-visibility.context.tsx @@ -19,6 +19,7 @@ import { useAuth } from './auth.context' import { CurrencyProvider } from './currency.context' import { showToast } from '@/common/toast' import { YadkarWidget } from '@/layouts/widgets/yadkar/yadkar' +import { HabitsLayout } from '@/layouts/widgets/habit/habits.layout' export enum WidgetKeys { comboWidget = 'comboWidget', @@ -33,6 +34,7 @@ export enum WidgetKeys { wigiPad = 'wigiPad', network = 'network', yadKar = 'yadKar', + HabitTracker = 'HabitTracker', } export interface WidgetItem { id: WidgetKeys @@ -126,6 +128,15 @@ export const widgetItems: WidgetItem[] = [ canToggle: true, isNew: false, }, + { + id: WidgetKeys.HabitTracker, + emoji: '🎯', + label: 'عادات', + order: 10, + node: , + canToggle: true, + isNew: true, + }, ] interface WidgetVisibilityContextType { diff --git a/src/layouts/widgets/habit/components/habit-form.modal.tsx b/src/layouts/widgets/habit/components/habit-form.modal.tsx new file mode 100644 index 00000000..7945d77d --- /dev/null +++ b/src/layouts/widgets/habit/components/habit-form.modal.tsx @@ -0,0 +1,316 @@ +import { useEffect, useState } from 'react' +import Analytics from '@/analytics' +import { + HABIT_COLOR_PRESETS, + HABIT_COMPARISON_OPTIONS, + HABIT_EMOJI_PRESETS, + HABIT_FREQUENCY_OPTIONS, + HABIT_UNIT_OPTIONS, +} from '@/common/constant/habit-options' +import { showToast } from '@/common/toast' +import { Button } from '@/components/button/button' +import Modal from '@/components/modal' +import { SelectBox } from '@/components/selectbox/selectbox' +import { TextInput } from '@/components/text-input' +import { safeAwait } from '@/services/api' +import { useAddHabit } from '@/services/hooks/habit/add-habit.hook' +import { + type CreateHabitInput, + type Habit, + HabitComparison, + HabitFrequency, + HabitUnit, +} from '@/services/hooks/habit/habit.interface' +import { useUpdateHabit } from '@/services/hooks/habit/update-habit.hook' +import { translateError } from '@/utils/translate-error' +import Tooltip from '@/components/toolTip' +import { BiInfoCircle } from 'react-icons/bi' + +interface HabitFormModalProps { + isOpen: boolean + habit: Habit | null + onClose: () => void + onSaved: () => void +} + +const defaultForm: CreateHabitInput = { + title: '', + emoji: HABIT_EMOJI_PRESETS[0], + color: HABIT_COLOR_PRESETS[0], + comparison: HabitComparison.AT_LEAST, + unit: HabitUnit.TIMES, + customUnit: '', + target: 1, + frequency: HabitFrequency.DAILY, + frequencyCount: 1, +} + +export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormModalProps) { + const isEdit = !!habit + const [form, setForm] = useState(defaultForm) + const { mutateAsync: addHabit, isPending: isAdding } = useAddHabit() + const { mutateAsync: updateHabit, isPending: isUpdating } = useUpdateHabit() + const isPending = isAdding || isUpdating + + useEffect(() => { + if (!isOpen) return + + if (habit) { + setForm({ + title: habit.title, + emoji: habit.emoji || HABIT_EMOJI_PRESETS[0], + color: habit.color || HABIT_COLOR_PRESETS[0], + comparison: habit.comparison, + unit: habit.unit, + customUnit: habit.customUnit || '', + target: habit.target, + frequency: habit.frequency, + frequencyCount: habit.frequencyCount, + }) + } else { + setForm(defaultForm) + } + }, [habit, isOpen]) + + const updateField = ( + key: K, + value: CreateHabitInput[K] + ) => { + setForm((prev) => ({ ...prev, [key]: value })) + } + + const handleSubmit = async () => { + if (isPending) return + + if (!form.title.trim()) { + showToast('عنوان عادت را وارد کنید.', 'error') + return + } + + if (form.unit === HabitUnit.CUSTOM && !form.customUnit?.trim()) { + showToast('واحد دلخواه را وارد کنید.', 'error') + return + } + + const payload: CreateHabitInput = { + ...form, + customUnit: form.unit === HabitUnit.CUSTOM ? form.customUnit : undefined, + frequencyCount: + form.frequency === HabitFrequency.DAILY ? 1 : form.frequencyCount || 1, + } + + const [error] = await safeAwait( + isEdit ? updateHabit({ id: habit!.id, input: payload }) : addHabit(payload) + ) + + if (error) { + showToast(translateError(error) as string, 'error') + return + } + + showToast(isEdit ? 'عادت ویرایش شد.' : 'عادت جدید اضافه شد.', 'success') + Analytics.event(isEdit ? 'habit_updated' : 'habit_created') + onSaved() + } + + return ( + +
+
+ + updateField('title', value)} + placeholder="مثلا نوشیدن آب" + direction="rtl" + /> +
+ +
+
+ +
+ {HABIT_EMOJI_PRESETS.map((emoji) => ( + + ))} +
+
+
+ +
+ {HABIT_COLOR_PRESETS.map((color) => ( +
+
+
+ +
+
+
+
+ + + + +
+ + updateField('unit', value as HabitUnit) + } + className="!w-full h-8!" + /> +
+
+
+ + + + +
+ + updateField('comparison', value as HabitComparison) + } + className="!w-full h-8!" + /> +
+
+ + {form.unit === HabitUnit.CUSTOM && ( +
+ updateField('customUnit', value)} + placeholder="نام واحد (مثلا: کیلومتر)" + direction="rtl" + className="h-8!" + /> +
+ )} + +
+
+
+ + + + +
+ + updateField('target', Number(value) || 0) + } + /> +
+
+
+ + + + +
+ + updateField('frequency', value as HabitFrequency) + } + className="!w-full h-8!" + /> +
+
+ + {form.frequency !== HabitFrequency.DAILY && ( +
+
+ + + + +
+ + updateField('frequencyCount', Number(value) || 1) + } + className="h-8!" + /> +
+ )} +
+ + +
+
+ ) +} diff --git a/src/layouts/widgets/habit/components/habit.item.tsx b/src/layouts/widgets/habit/components/habit.item.tsx new file mode 100644 index 00000000..b51438af --- /dev/null +++ b/src/layouts/widgets/habit/components/habit.item.tsx @@ -0,0 +1,142 @@ +import { FiArchive, FiCheck, FiEdit3 } from 'react-icons/fi' +import Analytics from '@/analytics' +import { getContrastingTextColor } from '@/common/color' +import { playAlarm } from '@/common/playAlarm' +import { showToast } from '@/common/toast' +import type { WidgetifyDate } from '@/layouts/widgets/calendar/utils' +import { safeAwait } from '@/services/api' +import { HabitComparison, type Habit } from '@/services/hooks/habit/habit.interface' +import { useLogHabitProgress } from '@/services/hooks/habit/log-habit-progress.hook' +import { translateError } from '@/utils/translate-error' +import { HABIT_UNIT_STEP } from '../../../../common/constant/habit-options' +import { formatHabitGoal } from '../utils/habit.utils' + +interface HabitItemProps { + habit: Habit + today: WidgetifyDate + onChanged: () => void + onEdit: () => void + onArchive: () => void +} + +export function HabitItem({ + habit, + today, + onChanged, + onEdit, + onArchive, +}: HabitItemProps) { + const { mutateAsync: logProgress, isPending } = useLogHabitProgress() + const isDone = habit.today.isDone + const color = habit.color || '#3b82f6' + const target = habit.target || 1 + + const handleQuickLog = async () => { + if (isPending) return + const date = today.clone().doAsGregorian().format('YYYY-MM-DD') + let step = HABIT_UNIT_STEP[habit.unit] || 1 + + if ( + habit.comparison === HabitComparison.EXACT && + habit.today.value + 1 > habit.target + ) { + step = 0 + } + + if ( + habit.comparison === HabitComparison.AT_MOST && + habit.today.value + 1 > habit.target + ) { + return showToast( + `مقدار فعلی به حداکثرِ هدفِ شما (یعنی ${habit.target}) رسیده.`, + 'error' + ) + } + + const [error] = await safeAwait( + logProgress({ id: habit.id, input: { date, amount: step } }) + ) + + if (error) { + showToast(translateError(error) as string, 'error') + return + } + + if (!isDone) playAlarm('success') + Analytics.event('habit_quick_log') + onChanged() + } + + return ( +
+
+
+ {habit.emoji || '🎯'} +
+
+

+ {habit.title} +

+

+ {formatHabitGoal(habit)} +

+
+
+ + + +
+ + {habit.today.value}/{habit.target} + +
+
+ {habit.history.map((day) => { + const dayProgress = Math.min(day.value / target, 1) + return ( + + ) + })} +
+
+ ) +} diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx new file mode 100644 index 00000000..a51daaba --- /dev/null +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -0,0 +1,190 @@ +import { useEffect, useRef, useState } from 'react' +import { MdRefresh } from 'react-icons/md' +import Analytics from '@/analytics' +import { showToast } from '@/common/toast' +import { Button } from '@/components/button/button' +import { IconLoading } from '@/components/loading/icon-loading' +import { ConfirmationModal } from '@/components/modal/confirmation-modal' +import { RequireAuth } from '@/components/auth/require-auth' +import Tooltip from '@/components/toolTip' +import { useAuth } from '@/context/auth.context' +import { useDate } from '@/context/date.context' +import { useGeneralSetting } from '@/context/general-setting.context' +import { safeAwait } from '@/services/api' +import { useArchiveHabit } from '@/services/hooks/habit/archive-habit.hook' +import { useGetHabits } from '@/services/hooks/habit/get-habits.hook' +import type { Habit } from '@/services/hooks/habit/habit.interface' +import { translateError } from '@/utils/translate-error' +import { WidgetContainer } from '../widget-container' +import { HabitFormModal } from './components/habit-form.modal' +import { HabitItem } from './components/habit.item' +import { CgAdd, CgAddR } from 'react-icons/cg' +import { BiPlus } from 'react-icons/bi' + +export function HabitsContent() { + const { isAuthenticated } = useAuth() + const { today } = useDate() + const { blurMode } = useGeneralSetting() + + const [editingHabit, setEditingHabit] = useState(null) + const [showForm, setShowForm] = useState(false) + const [archiveConfirm, setArchiveConfirm] = useState(null) + + const { data, isLoading, refetch } = useGetHabits(isAuthenticated) + const { mutateAsync: archiveHabit, isPending: isArchiving } = useArchiveHabit() + + const handleCloseForm = () => { + setShowForm(false) + setEditingHabit(null) + } + + const handleAddHabit = () => { + setEditingHabit(null) + setShowForm(true) + Analytics.event('habit_form_opened') + } + + const handleEditHabit = (habit: Habit) => { + setEditingHabit(habit) + setShowForm(true) + Analytics.event('habit_edit_opened') + } + + const handleConfirmArchive = async () => { + if (!archiveConfirm || isArchiving) return + + const [error] = await safeAwait(archiveHabit(archiveConfirm)) + + if (error) { + showToast(translateError(error) as string, 'error') + return + } + + setArchiveConfirm(null) + showToast('عادت حذف شد.', 'success') + Analytics.event('habit_archived') + refetch() + } + + const onRefresh = () => { + refetch() + Analytics.event('habit_refetch') + } + + return ( + <> +
+
+
+

🎯 عادت‌ها

+ + + آزمایشی + +
+ +
+ {isLoading ? : null} + + + + +
+
+
+ +
+
+ {isLoading ? ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) : data.items.length === 0 ? ( +
+
+ 🎯 +
+

+ هیچ عادتی برای نمایش وجود ندارد +

+

+ یک عادت جدید اضافه کن تا شروع کنی +

+
+ ) : ( +
+ {data.items.map((habit) => ( + handleEditHabit(habit)} + onArchive={() => setArchiveConfirm(habit.id)} + /> + ))} +
+ )} +
+
+ + + + { + handleCloseForm() + refetch() + }} + /> + + setArchiveConfirm(null)} + onConfirm={handleConfirmArchive} + variant="danger" + title="آرشیو عادت" + message="آیا از آرشیو این عادت اطمینان داری؟" + confirmText="آرشیو" + cancelText="انصراف" + isLoading={isArchiving} + /> + + ) +} + +export function HabitsLayout() { + return ( + + + + + + ) +} diff --git a/src/layouts/widgets/habit/utils/habit.utils.ts b/src/layouts/widgets/habit/utils/habit.utils.ts new file mode 100644 index 00000000..4d89d5ae --- /dev/null +++ b/src/layouts/widgets/habit/utils/habit.utils.ts @@ -0,0 +1,45 @@ +import { + HabitFrequency, + HabitUnit, + type Habit, +} from '@/services/hooks/habit/habit.interface' + +const unitLabels: Record = { + [HabitUnit.TIMES]: 'دفعه', + [HabitUnit.MINUTES]: 'دقیقه', + [HabitUnit.HOURS]: 'ساعت', + [HabitUnit.PAGES]: 'صفحه', + [HabitUnit.GLASSES]: 'لیوان', + [HabitUnit.CUSTOM]: '', +} + +const comparisonLabels: Record = { + AT_LEAST: 'حداقل', + AT_MOST: 'حداکثر', + EXACT: 'دقیقا', +} + +const frequencyLabels: Record = { + [HabitFrequency.DAILY]: 'روزانه', + [HabitFrequency.WEEKLY]: 'هفته', + [HabitFrequency.MONTHLY]: 'ماه', +} + +export function getHabitUnitLabel(habit: Habit): string { + if (habit.unit === HabitUnit.CUSTOM) { + return habit.customUnit || '' + } + return unitLabels[habit.unit] || '' +} + +export function formatHabitGoal(habit: Habit): string { + const unitLabel = getHabitUnitLabel(habit) + const base = + `${comparisonLabels[habit.comparison]} ${habit.target} ${unitLabel}`.trim() + + if (habit.frequency === HabitFrequency.DAILY) { + return `${base} در روز` + } + + return `${base} · ${habit.progressThisPeriod.done} از ${habit.progressThisPeriod.required} بار در ${frequencyLabels[habit.frequency]}` +} diff --git a/src/services/hooks/habit/add-habit.hook.ts b/src/services/hooks/habit/add-habit.hook.ts new file mode 100644 index 00000000..d2ea631f --- /dev/null +++ b/src/services/hooks/habit/add-habit.hook.ts @@ -0,0 +1,14 @@ +import { useMutation } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' +import type { CreateHabitInput } from './habit.interface' + +export const useAddHabit = () => { + return useMutation({ + mutationKey: ['addHabit'], + mutationFn: async (input: CreateHabitInput) => { + const client = await getMainClient() + const response = await client.post('/widgets/habits', input) + return response.data + }, + }) +} diff --git a/src/services/hooks/habit/archive-habit.hook.ts b/src/services/hooks/habit/archive-habit.hook.ts new file mode 100644 index 00000000..b3c11d67 --- /dev/null +++ b/src/services/hooks/habit/archive-habit.hook.ts @@ -0,0 +1,13 @@ +import { useMutation } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' + +export const useArchiveHabit = () => { + return useMutation({ + mutationKey: ['archiveHabit'], + mutationFn: async (id: string) => { + const client = await getMainClient() + const response = await client.delete(`/widgets/habits/${id}`) + return response.data + }, + }) +} diff --git a/src/services/hooks/habit/get-habits.hook.ts b/src/services/hooks/habit/get-habits.hook.ts new file mode 100644 index 00000000..223431f4 --- /dev/null +++ b/src/services/hooks/habit/get-habits.hook.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' +import { Habit } from './habit.interface' + +interface GetHabitsResponse { + items: Habit[] + page: number + limit: number + total: number +} + +export const useGetHabits = (enabled: boolean, archived = false) => { + return useQuery({ + queryKey: ['get-habits', archived], + queryFn: () => getHabits(archived), + retry: 0, + enabled, + initialData: { + items: [], + page: 1, + limit: 20, + total: 0, + }, + refetchOnWindowFocus: false, + }) +} + +async function getHabits(archived: boolean): Promise { + const client = await getMainClient() + const { data } = await client.get<{ data: GetHabitsResponse }>('/widgets/habits', { + params: { archived, limit: 50 }, + }) + return data.data +} diff --git a/src/services/hooks/habit/habit.interface.ts b/src/services/hooks/habit/habit.interface.ts new file mode 100644 index 00000000..0d8b6b08 --- /dev/null +++ b/src/services/hooks/habit/habit.interface.ts @@ -0,0 +1,68 @@ +export enum HabitComparison { + AT_LEAST = 'AT_LEAST', + AT_MOST = 'AT_MOST', + EXACT = 'EXACT', +} + +export enum HabitUnit { + TIMES = 'TIMES', + MINUTES = 'MINUTES', + HOURS = 'HOURS', + PAGES = 'PAGES', + GLASSES = 'GLASSES', + CUSTOM = 'CUSTOM', +} + +export enum HabitFrequency { + DAILY = 'DAILY', + WEEKLY = 'WEEKLY', + MONTHLY = 'MONTHLY', +} + +export interface HabitDayProgress { + date: string + value: number + isDone: boolean +} + +export interface Habit { + id: string + title: string + emoji: string | null + color: string | null + target: number + comparison: HabitComparison + unit: HabitUnit + customUnit: string | null + frequency: HabitFrequency + frequencyCount: number + sort: number + archivedAt: string | null + today: HabitDayProgress + history: HabitDayProgress[] + progressThisPeriod: { + done: number + required: number + } +} + +export interface CreateHabitInput { + title: string + emoji?: string + color?: string + comparison: HabitComparison + unit: HabitUnit + customUnit?: string + target: number + frequency: HabitFrequency + frequencyCount?: number +} + +export interface UpdateHabitInput extends Partial { + sort?: number +} + +export interface LogHabitProgressInput { + date: string + amount: number +} \ No newline at end of file diff --git a/src/services/hooks/habit/log-habit-progress.hook.ts b/src/services/hooks/habit/log-habit-progress.hook.ts new file mode 100644 index 00000000..21a0ff3e --- /dev/null +++ b/src/services/hooks/habit/log-habit-progress.hook.ts @@ -0,0 +1,20 @@ +import { useMutation } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' +import type { LogHabitProgressInput } from './habit.interface' + +export const useLogHabitProgress = () => { + return useMutation({ + mutationKey: ['logHabitProgress'], + mutationFn: async ({ + id, + input, + }: { + id: string + input: LogHabitProgressInput + }) => { + const client = await getMainClient() + const response = await client.put(`/widgets/habits/${id}/progress`, input) + return response.data + }, + }) +} diff --git a/src/services/hooks/habit/update-habit.hook.ts b/src/services/hooks/habit/update-habit.hook.ts new file mode 100644 index 00000000..7980439a --- /dev/null +++ b/src/services/hooks/habit/update-habit.hook.ts @@ -0,0 +1,14 @@ +import { useMutation } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' +import type { UpdateHabitInput } from './habit.interface' + +export const useUpdateHabit = () => { + return useMutation({ + mutationKey: ['updateHabit'], + mutationFn: async ({ id, input }: { id: string; input: UpdateHabitInput }) => { + const client = await getMainClient() + const response = await client.patch(`/widgets/habits/${id}`, input) + return response.data + }, + }) +} From 11ae44bc50a59d9b64b02df1c288589bcad6bd80 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 15:42:54 +0330 Subject: [PATCH 04/17] details --- .../components/habit-calendar-heatmap.tsx | 112 ++++++++++++++++++ .../habit/components/habit-detail.modal.tsx | 64 ++++++++++ .../widgets/habit/components/habit.item.tsx | 32 +++-- src/layouts/widgets/habit/habits.layout.tsx | 14 ++- src/layouts/widgets/habit/utils.ts | 45 +++++++ src/services/hooks/habit/habit.interface.ts | 1 + 6 files changed, 257 insertions(+), 11 deletions(-) create mode 100644 src/layouts/widgets/habit/components/habit-calendar-heatmap.tsx create mode 100644 src/layouts/widgets/habit/components/habit-detail.modal.tsx create mode 100644 src/layouts/widgets/habit/utils.ts diff --git a/src/layouts/widgets/habit/components/habit-calendar-heatmap.tsx b/src/layouts/widgets/habit/components/habit-calendar-heatmap.tsx new file mode 100644 index 00000000..fe6b5b5c --- /dev/null +++ b/src/layouts/widgets/habit/components/habit-calendar-heatmap.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react' +import { IoChevronBack, IoChevronForward } from 'react-icons/io5' +import type { Habit } from '@/services/hooks/habit/habit.interface' + +const PERSIAN_MONTHS = [ + 'فروردین', + 'اردیبهشت', + 'خرداد', + 'تیر', + 'مرداد', + 'شهریور', + 'مهر', + 'آبان', + 'آذر', + 'دی', + 'بهمن', + 'اسفند', +] + +const DAYS = ['ش', 'ی', 'د', 'س', 'چ', 'ج', 'ج'] + +interface HeatmapProps { + habit: Habit + color: string +} + +export function HabitCalendarHeatmap({ habit, color }: HeatmapProps) { + const today = new Date() + today.setHours(0, 0, 0, 0) + const currentYear = today.getFullYear() + const currentMonth = today.getMonth() + + const [month, setMonth] = useState(currentMonth) + + const monthKey = `${currentYear}-${String(month + 1).padStart(2, '0')}` + const monthData = habit.calendarData[monthKey] || {} + const daysInMonth = new Date(currentYear, month + 1, 0).getDate() + const firstDay = new Date(currentYear, month, 1).getDay() + + const weeks: (string | null)[][] = [[]] + const offset = firstDay === 0 ? 6 : firstDay - 1 + + for (let i = 0; i < offset; i++) weeks[0].push(null) + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${currentYear}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` + if (weeks[weeks.length - 1].length === 7) weeks.push([]) + weeks[weeks.length - 1].push(dateStr) + } + + const canNext = month < currentMonth + const canPrev = month > 0 + + return ( +
+
+ + + {PERSIAN_MONTHS[month]} + + +
+ +
+ {DAYS.map((d) => ( +
+ {d} +
+ ))} +
+ +
+ {weeks.map((week, i) => ( +
+ {week.map((date, j) => { + if (!date) return
+ const data = monthData[date] + const isDone = data?.isDone ?? false + return ( +
+ ) + })} +
+ ))} +
+
+ ) +} diff --git a/src/layouts/widgets/habit/components/habit-detail.modal.tsx b/src/layouts/widgets/habit/components/habit-detail.modal.tsx new file mode 100644 index 00000000..2b99131d --- /dev/null +++ b/src/layouts/widgets/habit/components/habit-detail.modal.tsx @@ -0,0 +1,64 @@ +import { FiX } from 'react-icons/fi' +import Modal from '@/components/modal' +import type { Habit } from '@/services/hooks/habit/habit.interface' +import { HabitCalendarHeatmap } from './habit-calendar-heatmap' + +interface ModalProps { + isOpen: boolean + habit: Habit | null + onClose: () => void +} + +export function HabitDetailModal({ isOpen, habit, onClose }: ModalProps) { + if (!habit) return null + + const color = habit.color || '#3b82f6' + const weekDone = habit.history.filter((h) => h.isDone).length + + return ( + +
+
+
{habit.emoji || '🎯'}
+ +
+ +
+

{habit.title}

+

+ هدف: {habit.target} {habit.customUnit || ''} +

+
+ +
+
+

امروز

+

+ {habit.today.value}/{habit.target} +

+
+
+

7 روز

+

{weekDone}/7

+
+
+

این دوره

+

+ {habit.progressThisPeriod.done}/ + {habit.progressThisPeriod.required} +

+
+
+ +
+ +
+
+
+ ) +} diff --git a/src/layouts/widgets/habit/components/habit.item.tsx b/src/layouts/widgets/habit/components/habit.item.tsx index b51438af..c73fce63 100644 --- a/src/layouts/widgets/habit/components/habit.item.tsx +++ b/src/layouts/widgets/habit/components/habit.item.tsx @@ -8,8 +8,8 @@ import { safeAwait } from '@/services/api' import { HabitComparison, type Habit } from '@/services/hooks/habit/habit.interface' import { useLogHabitProgress } from '@/services/hooks/habit/log-habit-progress.hook' import { translateError } from '@/utils/translate-error' -import { HABIT_UNIT_STEP } from '../../../../common/constant/habit-options' -import { formatHabitGoal } from '../utils/habit.utils' +import { HABIT_UNIT_STEP } from '@/common/constant/habit-options' +import { formatHabitGoal } from '../utils' interface HabitItemProps { habit: Habit @@ -17,6 +17,7 @@ interface HabitItemProps { onChanged: () => void onEdit: () => void onArchive: () => void + onViewDetails: () => void } export function HabitItem({ @@ -25,13 +26,15 @@ export function HabitItem({ onChanged, onEdit, onArchive, + onViewDetails, }: HabitItemProps) { const { mutateAsync: logProgress, isPending } = useLogHabitProgress() const isDone = habit.today.isDone const color = habit.color || '#3b82f6' const target = habit.target || 1 - const handleQuickLog = async () => { + const handleQuickLog = async (e?: React.MouseEvent) => { + if (e) e.stopPropagation() if (isPending) return const date = today.clone().doAsGregorian().format('YYYY-MM-DD') let step = HABIT_UNIT_STEP[habit.unit] || 1 @@ -68,7 +71,11 @@ export function HabitItem({ } return ( -
+
-
+ ) } diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx index a51daaba..ef68ccf3 100644 --- a/src/layouts/widgets/habit/habits.layout.tsx +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -16,9 +16,9 @@ import { useGetHabits } from '@/services/hooks/habit/get-habits.hook' import type { Habit } from '@/services/hooks/habit/habit.interface' import { translateError } from '@/utils/translate-error' import { WidgetContainer } from '../widget-container' +import { HabitDetailModal } from './components/habit-detail.modal' import { HabitFormModal } from './components/habit-form.modal' import { HabitItem } from './components/habit.item' -import { CgAdd, CgAddR } from 'react-icons/cg' import { BiPlus } from 'react-icons/bi' export function HabitsContent() { @@ -27,6 +27,7 @@ export function HabitsContent() { const { blurMode } = useGeneralSetting() const [editingHabit, setEditingHabit] = useState(null) + const [detailHabit, setDetailHabit] = useState(null) const [showForm, setShowForm] = useState(false) const [archiveConfirm, setArchiveConfirm] = useState(null) @@ -103,7 +104,7 @@ export function HabitsContent() {
-
+
@@ -138,6 +139,7 @@ export function HabitsContent() { onChanged={refetch} onEdit={() => handleEditHabit(habit)} onArchive={() => setArchiveConfirm(habit.id)} + onViewDetails={() => setDetailHabit(habit)} /> ))}
@@ -148,7 +150,7 @@ export function HabitsContent() { - - {PERSIAN_MONTHS[month]} - - +
+
+

+ {currentDate.format('dddd، jD jMMMM jYYYY')}{' '} +

+
+ {isUpdating ? : null} + {showTodayButton && ( + + )} + + +
-
- {DAYS.map((d) => ( +
+ {WEEKDAYS.map((weekday) => (
- {d} + {weekday}
))}
-
- {weeks.map((week, i) => ( -
- {week.map((date, j) => { - if (!date) return
- const data = monthData[date] - const isDone = data?.isDone ?? false - return ( -
- ) - })} -
- ))} -
+
{renderCalendarGrid()}
) } diff --git a/src/layouts/widgets/habit/components/habit-detail.modal.tsx b/src/layouts/widgets/habit/components/habit-detail.modal.tsx index 2b99131d..e27f63ce 100644 --- a/src/layouts/widgets/habit/components/habit-detail.modal.tsx +++ b/src/layouts/widgets/habit/components/habit-detail.modal.tsx @@ -1,64 +1,86 @@ -import { FiX } from 'react-icons/fi' import Modal from '@/components/modal' -import type { Habit } from '@/services/hooks/habit/habit.interface' -import { HabitCalendarHeatmap } from './habit-calendar-heatmap' +import { IconLoading } from '@/components/loading/icon-loading' +import { useGetHabitDetail } from '@/services/hooks/habit/get-habit-detail.hook' +import { HabitCalendar } from './habit-calendar-heatmap' +import { useAuth } from '@/context/auth.context' +import { formatHabitGoal } from '../utils' interface ModalProps { isOpen: boolean - habit: Habit | null + habitId: string | null onClose: () => void } -export function HabitDetailModal({ isOpen, habit, onClose }: ModalProps) { +export function HabitDetailModal({ isOpen, habitId, onClose }: ModalProps) { + const { isAuthenticated } = useAuth() + const { data: habit, isLoading } = useGetHabitDetail( + habitId || '', + isOpen && isAuthenticated + ) + if (!habit) return null const color = habit.color || '#3b82f6' const weekDone = habit.history.filter((h) => h.isDone).length return ( - -
-
-
{habit.emoji || '🎯'}
- -
- -
-

{habit.title}

-

- هدف: {habit.target} {habit.customUnit || ''} -

-
- -
-
-

امروز

-

- {habit.today.value}/{habit.target} -

-
-
-

7 روز

-

{weekDone}/7

+ +
+ {habit.emoji || '🎯'}
-
-

این دوره

-

- {habit.progressThisPeriod.done}/ - {habit.progressThisPeriod.required} +

+

+ {habit.title} +

+

+ {formatHabitGoal(habit)}

+ } + > + {isLoading ? ( +
+ +
+ ) : ( +
+
+
+

امروز

+

+ {habit.today.value}/{habit.target} +

+
+
+

7 روز

+

{weekDone}/7

+
+
+

این دوره

+

+ {habit.progressThisPeriod.done}/ + {habit.progressThisPeriod.required} +

+
+
-
- +
+ +
-
+ )}
) } diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx index ef68ccf3..0fb602cd 100644 --- a/src/layouts/widgets/habit/habits.layout.tsx +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -1,9 +1,8 @@ -import { useEffect, useRef, useState } from 'react' +import { useState } from 'react' import { MdRefresh } from 'react-icons/md' import Analytics from '@/analytics' import { showToast } from '@/common/toast' import { Button } from '@/components/button/button' -import { IconLoading } from '@/components/loading/icon-loading' import { ConfirmationModal } from '@/components/modal/confirmation-modal' import { RequireAuth } from '@/components/auth/require-auth' import Tooltip from '@/components/toolTip' @@ -27,11 +26,11 @@ export function HabitsContent() { const { blurMode } = useGeneralSetting() const [editingHabit, setEditingHabit] = useState(null) - const [detailHabit, setDetailHabit] = useState(null) + const [detailHabitId, setDetailHabitId] = useState(null) const [showForm, setShowForm] = useState(false) const [archiveConfirm, setArchiveConfirm] = useState(null) - const { data, isLoading, refetch } = useGetHabits(isAuthenticated) + const { data, isLoading, refetch, isRefetching } = useGetHabits(isAuthenticated) const { mutateAsync: archiveHabit, isPending: isArchiving } = useArchiveHabit() const handleCloseForm = () => { @@ -72,6 +71,14 @@ export function HabitsContent() { Analytics.event('habit_refetch') } + const onCloseDetailModal = () => { + setDetailHabitId(null) + refetch() + Analytics.event('habit_close_detail_model') + } + + const isWaiting = isLoading || isRefetching + return ( <>
@@ -85,17 +92,16 @@ export function HabitsContent() {
- {isLoading ? : null} - @@ -139,7 +145,7 @@ export function HabitsContent() { onChanged={refetch} onEdit={() => handleEditHabit(habit)} onArchive={() => setArchiveConfirm(habit.id)} - onViewDetails={() => setDetailHabit(habit)} + onViewDetails={() => setDetailHabitId(habit.id)} /> ))}
@@ -167,9 +173,9 @@ export function HabitsContent() { /> setDetailHabit(null)} + isOpen={!!detailHabitId} + habitId={detailHabitId} + onClose={() => onCloseDetailModal()} /> { + return useQuery({ + queryKey: ['get-habit-detail', habitId], + queryFn: () => getHabitDetail(habitId), + retry: 0, + enabled: enabled && !!habitId, + refetchOnWindowFocus: false, + }) +} + +async function getHabitDetail(habitId: string): Promise { + const client = await getMainClient() + const { data } = await client.get<{ data: Habit }>(`/widgets/habits/${habitId}`) + return data.data +} From 4bf45abcb6b3034c2355ee897edfca174d4ceb49 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 18:13:34 +0330 Subject: [PATCH 06/17] improve clickable --- src/layouts/widgets/habit/components/habit.item.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/widgets/habit/components/habit.item.tsx b/src/layouts/widgets/habit/components/habit.item.tsx index c73fce63..395a85a0 100644 --- a/src/layouts/widgets/habit/components/habit.item.tsx +++ b/src/layouts/widgets/habit/components/habit.item.tsx @@ -78,7 +78,7 @@ export function HabitItem({ >
{habit.emoji || '🎯'}
-
+

{habit.title}

From 33ffacdd1c0da8455f04fd5fde2f16fff07d89e0 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 21:07:29 +0330 Subject: [PATCH 07/17] add icons & colors --- .../habit/components/habit-form.modal.tsx | 39 ++++++++++++------- src/layouts/widgets/habit/habits.layout.tsx | 6 ++- src/services/hooks/habit/get-habits.hook.ts | 14 +++---- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/layouts/widgets/habit/components/habit-form.modal.tsx b/src/layouts/widgets/habit/components/habit-form.modal.tsx index 7945d77d..fed38619 100644 --- a/src/layouts/widgets/habit/components/habit-form.modal.tsx +++ b/src/layouts/widgets/habit/components/habit-form.modal.tsx @@ -25,17 +25,19 @@ import { useUpdateHabit } from '@/services/hooks/habit/update-habit.hook' import { translateError } from '@/utils/translate-error' import Tooltip from '@/components/toolTip' import { BiInfoCircle } from 'react-icons/bi' +import type { HabitIcon } from '@/services/hooks/habit/get-habits.hook' interface HabitFormModalProps { isOpen: boolean habit: Habit | null onClose: () => void onSaved: () => void + icons: HabitIcon[] + colors: string[] } const defaultForm: CreateHabitInput = { title: '', - emoji: HABIT_EMOJI_PRESETS[0], color: HABIT_COLOR_PRESETS[0], comparison: HabitComparison.AT_LEAST, unit: HabitUnit.TIMES, @@ -45,7 +47,14 @@ const defaultForm: CreateHabitInput = { frequencyCount: 1, } -export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormModalProps) { +export function HabitFormModal({ + isOpen, + habit, + onClose, + onSaved, + icons, + colors, +}: HabitFormModalProps) { const isEdit = !!habit const [form, setForm] = useState(defaultForm) const { mutateAsync: addHabit, isPending: isAdding } = useAddHabit() @@ -136,27 +145,27 @@ export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormMod
-
- {HABIT_EMOJI_PRESETS.map((emoji) => ( +
+ {icons.map((emoji) => ( ))}
-
- {HABIT_COLOR_PRESETS.map((color) => ( +
+ {colors.map((color) => (
@@ -204,7 +213,7 @@ export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormMod
@@ -238,7 +247,7 @@ export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormMod
@@ -258,7 +267,7 @@ export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormMod
@@ -284,7 +293,7 @@ export function HabitFormModal({ isOpen, habit, onClose, onSaved }: HabitFormMod >
diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx index 0fb602cd..2734df12 100644 --- a/src/layouts/widgets/habit/habits.layout.tsx +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -123,7 +123,7 @@ export function HabitsContent() { /> ))}
- ) : data.items.length === 0 ? ( + ) : data?.items?.length === 0 ? (
🎯 @@ -137,7 +137,7 @@ export function HabitsContent() {
) : (
- {data.items.map((habit) => ( + {data?.items?.map((habit) => ( { @@ -15,12 +21,6 @@ export const useGetHabits = (enabled: boolean, archived = false) => { queryFn: () => getHabits(archived), retry: 0, enabled, - initialData: { - items: [], - page: 1, - limit: 20, - total: 0, - }, refetchOnWindowFocus: false, }) } From c16b80c14e1556e0ccbb1052b3854acd850c5c68 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 22:12:48 +0330 Subject: [PATCH 08/17] improve errors --- src/context/widget-visibility.context.tsx | 2 +- src/layouts/widgets/habit/habits.layout.tsx | 19 ++++++---------- src/layouts/widgets/yadkar/yadkar.tsx | 25 +++++++++++++++++---- src/utils/translate-error.ts | 2 ++ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/context/widget-visibility.context.tsx b/src/context/widget-visibility.context.tsx index 18c425e0..c45707ba 100644 --- a/src/context/widget-visibility.context.tsx +++ b/src/context/widget-visibility.context.tsx @@ -66,7 +66,7 @@ export const widgetItems: WidgetItem[] = [ order: 0, node: , canToggle: true, - isNew: true, + isNew: false, }, { id: WidgetKeys.tools, diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx index 2734df12..3a017bbd 100644 --- a/src/layouts/widgets/habit/habits.layout.tsx +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -1,10 +1,9 @@ import { useState } from 'react' import { MdRefresh } from 'react-icons/md' import Analytics from '@/analytics' -import { showToast } from '@/common/toast' +import { autoFormatErrorToast, showToast } from '@/common/toast' import { Button } from '@/components/button/button' import { ConfirmationModal } from '@/components/modal/confirmation-modal' -import { RequireAuth } from '@/components/auth/require-auth' import Tooltip from '@/components/toolTip' import { useAuth } from '@/context/auth.context' import { useDate } from '@/context/date.context' @@ -13,7 +12,6 @@ import { safeAwait } from '@/services/api' import { useArchiveHabit } from '@/services/hooks/habit/archive-habit.hook' import { useGetHabits } from '@/services/hooks/habit/get-habits.hook' import type { Habit } from '@/services/hooks/habit/habit.interface' -import { translateError } from '@/utils/translate-error' import { WidgetContainer } from '../widget-container' import { HabitDetailModal } from './components/habit-detail.modal' import { HabitFormModal } from './components/habit-form.modal' @@ -56,7 +54,7 @@ export function HabitsContent() { const [error] = await safeAwait(archiveHabit(archiveConfirm)) if (error) { - showToast(translateError(error) as string, 'error') + autoFormatErrorToast(error) return } @@ -83,12 +81,11 @@ export function HabitsContent() { <>
-
-

🎯 عادت‌ها

- - +
+

عادت‌ها

+ {/* آزمایشی - + */}
@@ -198,9 +195,7 @@ export function HabitsContent() { export function HabitsLayout() { return ( - - - + ) } diff --git a/src/layouts/widgets/yadkar/yadkar.tsx b/src/layouts/widgets/yadkar/yadkar.tsx index 79727b93..059eaaf2 100644 --- a/src/layouts/widgets/yadkar/yadkar.tsx +++ b/src/layouts/widgets/yadkar/yadkar.tsx @@ -3,13 +3,19 @@ import Analytics from '@/analytics' import { NotesLayout } from '../notes/notes.layout' import { TodosLayout } from '../todos/todos' import { TabNavigation } from '@/components/tab-navigation' -import { HiOutlineCheckCircle, HiOutlineDocumentText } from 'react-icons/hi2' +import { + HiOutlineCheckCircle, + HiOutlineDocumentText, + HiOutlineFire, +} from 'react-icons/hi2' import { WidgetContainer } from '../widget-container' +import { HabitsContent } from '../habit/habits.layout' +type Tab = 'todos' | 'notes' | 'rabbit' export function YadkarWidget() { - const [tab, setTab] = useState<'todos' | 'notes'>('todos') + const [tab, setTab] = useState('todos') - const onChangeTab = (newTab: 'todos' | 'notes') => { + const onChangeTab = (newTab: Tab) => { setTab(newTab) Analytics.event('yadkar_change_tab') } @@ -34,6 +40,11 @@ export function YadkarWidget() { label: 'یادداشت', icon: , }, + { + id: 'rabbit', + label: 'عادت‌ها', + icon: , + }, ]} size="small" className="w-full border-none" @@ -41,7 +52,13 @@ export function YadkarWidget() {
- {tab === 'todos' ? : } + {tab === 'todos' ? ( + + ) : tab === 'notes' ? ( + + ) : ( + + )}
) diff --git a/src/utils/translate-error.ts b/src/utils/translate-error.ts index 5f65c13e..d74d3a63 100644 --- a/src/utils/translate-error.ts +++ b/src/utils/translate-error.ts @@ -108,6 +108,8 @@ const errorTranslations: Record = { PAYMENT_NOT_FOUND: 'پرداخت مورد نظر یافت نشد', TRY_NEXT_TIME: 'چیزی اشتباه پیش رفت، لطفا بعدا دوباره تلاش کنید', + + TOO_MANY_ATTEMPTS_HABIT: 'بیش از حد نمیتونید بسازید.', } const validationTranslations: Record = { From a64ebba2f8ad75a7662257b01ac3a9c4a5d610f0 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 22:23:38 +0330 Subject: [PATCH 09/17] change release note --- src/components/UpdateReleaseNotesModal.tsx | 203 +++++++-------------- 1 file changed, 71 insertions(+), 132 deletions(-) diff --git a/src/components/UpdateReleaseNotesModal.tsx b/src/components/UpdateReleaseNotesModal.tsx index 01ff8abd..6138fa29 100644 --- a/src/components/UpdateReleaseNotesModal.tsx +++ b/src/components/UpdateReleaseNotesModal.tsx @@ -1,67 +1,7 @@ import { useEffect, useState } from 'react' -import { - RiBug2Line, - RiCheckboxCircleLine, - RiThumbUpLine, - RiSparklingLine, -} from 'react-icons/ri' +import { RiCheckboxCircleLine, RiThumbUpLine } from 'react-icons/ri' import { Button } from './button/button' import Modal from './modal' -import { ConfigKey } from '@/common/constant/config.key' - -type MediaContent = { - type: 'image' | 'video' - url: string - caption?: string -} - -type ReleaseNote = { - type: 'feature' | 'bugfix' | 'improvement' | 'info' - title: string - description: string - media?: MediaContent[] -} - -const VERSION_NAME = ConfigKey.VERSION_NAME - -const releaseNotes: ReleaseNote[] = [ - { - type: 'feature', - title: 'امکان پیشنمایش در فروشگاه', - description: - 'الان دیگه قبل اینکه از فروشگاه والپیپر یا هرچیز دیگه ای بخری، میتونی پیشنمایش داشته باشی🐳', - }, - { - type: 'feature', - title: 'مدیریت اعلانات', - description: '', - }, - { - type: 'improvement', - title: 'بهبود ظاهری فروشگاه', - description: '', - }, - { - type: 'improvement', - title: 'بهبود قسمت سرچ های پیشنهادی', - description: '', - }, - { - type: 'improvement', - title: 'بهبود برنامک ها', - description: '', - }, - { - type: 'improvement', - description: '', - title: 'بهبود عملکرد کلی', - }, - { - type: 'bugfix', - description: '', - title: 'رفع مشکلات گزارش شده', - }, -] type UpdateReleaseNotesModalProps = { isOpen: boolean @@ -75,10 +15,10 @@ export const UpdateReleaseNotesModal = ({ counterValue, }: UpdateReleaseNotesModalProps) => { const [counter, setCounter] = useState(0) - + const videoRef = useRef(null) useEffect(() => { if (isOpen && counterValue !== null) { - setCounter(counterValue === null ? 2 : counterValue) + setCounter(counterValue === null ? 10 : counterValue) const interval = setInterval(() => { setCounter((prev) => { if (prev <= 1) { @@ -94,92 +34,91 @@ export const UpdateReleaseNotesModal = ({ } }, [isOpen]) - const getTypeIcon = (note: ReleaseNote) => { - switch (note.type) { - case 'feature': - return - case 'bugfix': - return - case 'improvement': - return - default: - return + useEffect(() => { + if (isOpen && videoRef.current) { + videoRef.current.play().catch(() => {}) } - } + }, [isOpen]) return ( -
-
-
+
+
+
+

+ آپدیت جدید، با ویجت جدید رسید! (نسخه آزمایشی) +

+
-
-
- {releaseNotes.map((note, index) => ( -
-
-
-
-

- {note.title} -

-
- {getTypeIcon(note)} -
-

- {note.description} -

-
- ))} +
+
+ +
+ +
+
+

+ ویجت عادت‌ها (Habit Tracker) اضافه شد! +

+
+ +

+ از امروز می‌تونی{' '} + + عادت‌های روزانه‌ات رو ثبت و دنبال کنی + + . هر روز که به هدفت پایبند باشی، زنجیره عادتت کامل‌تر میشه و + پیشرفتت همیشه جلوی چشمت خواهد بود. +

+ +
    + {[ + 'ثبت و پیگیری عادت‌های روزانه', + 'مشاهده میزان پایبندی و استریک‌ها', + 'اضافه کردن چندین عادت در یک ویجت', + ].map((item, i) => ( +
  • + + {item} +
  • + ))} +
-
- +
+ دمت گرم که همراه مایی
-
-
- - کانال و گروه ما در بله - - + +
+ +
) From e4eb983681af453cc70dff643bf034b6ea7f4403 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 22:26:19 +0330 Subject: [PATCH 10/17] improve notes --- src/common/constant/config.key.ts | 2 +- src/components/UpdateReleaseNotesModal.tsx | 29 +++++++--------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/common/constant/config.key.ts b/src/common/constant/config.key.ts index 394eb453..1c044de4 100644 --- a/src/common/constant/config.key.ts +++ b/src/common/constant/config.key.ts @@ -1,4 +1,4 @@ export enum ConfigKey { - VERSION_NAME = 'ساحل', + VERSION_NAME = 'مورچه', WIG_COIN_ICON = 'https://cdn.widgetify.ir/extension/wig-icon.png', } diff --git a/src/components/UpdateReleaseNotesModal.tsx b/src/components/UpdateReleaseNotesModal.tsx index 6138fa29..f1bc59ce 100644 --- a/src/components/UpdateReleaseNotesModal.tsx +++ b/src/components/UpdateReleaseNotesModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { RiCheckboxCircleLine, RiThumbUpLine } from 'react-icons/ri' +import { RiThumbUpLine } from 'react-icons/ri' import { Button } from './button/button' import Modal from './modal' @@ -44,7 +44,7 @@ export const UpdateReleaseNotesModal = ({ -
    - {[ - 'ثبت و پیگیری عادت‌های روزانه', - 'مشاهده میزان پایبندی و استریک‌ها', - 'اضافه کردن چندین عادت در یک ویجت', - ].map((item, i) => ( -
  • - - {item} -
  • - ))} -
+

+ از قسمت{' '} + مدیریت ویجت‌ها{' '} + می‌تونی این ویجت رو فعال کنی یا از ویجت{' '} + یادکار استفاده + کنی. +

From 3d2e230bd6a5e017e8b46169394f0e494e8e3216 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 22:38:31 +0330 Subject: [PATCH 11/17] improve widget badges --- src/common/constant/store.key.ts | 1 + src/context/widget-visibility.context.tsx | 4 +++- .../manage-widgets/manage-widgets.tsx | 12 ++++++++---- src/layouts/widgets/yadkar/yadkar.tsx | 16 +++++++++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index da0803b9..6ed0af69 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -86,4 +86,5 @@ export interface StorageKV { [key: `removed_notification_${string}`]: string selected_engine: string | null widget_tab: string + yadkar_tab: string } diff --git a/src/context/widget-visibility.context.tsx b/src/context/widget-visibility.context.tsx index c45707ba..3267b534 100644 --- a/src/context/widget-visibility.context.tsx +++ b/src/context/widget-visibility.context.tsx @@ -47,6 +47,7 @@ export interface WidgetItem { disabled?: boolean soon?: boolean popular?: boolean + isBeta?: boolean } export const widgetItems: WidgetItem[] = [ @@ -62,7 +63,7 @@ export const widgetItems: WidgetItem[] = [ { id: WidgetKeys.yadKar, emoji: '📒', - label: 'یادکار (وظایف و یادداشت)', + label: 'یادکار (وظایف/یادداشت/عادت‌ها)', order: 0, node: , canToggle: true, @@ -136,6 +137,7 @@ export const widgetItems: WidgetItem[] = [ node: , canToggle: true, isNew: true, + isBeta: true, }, ] diff --git a/src/layouts/widgets-settings/manage-widgets/manage-widgets.tsx b/src/layouts/widgets-settings/manage-widgets/manage-widgets.tsx index 95357d7a..7ea3586e 100644 --- a/src/layouts/widgets-settings/manage-widgets/manage-widgets.tsx +++ b/src/layouts/widgets-settings/manage-widgets/manage-widgets.tsx @@ -60,6 +60,7 @@ function WidgetItemComponent({ const isDisabled = widget.disabled || false const isSoon = widget.soon || false + const isBeta = widget.isBeta || false const finalCanToggle = canToggle && !isDisabled @@ -68,7 +69,7 @@ function WidgetItemComponent({ extraClasses += ' border-red-500/50 bg-red-500/10' } if (isSoon) { - extraClasses += ' border-warning/50 bg-warning/10' + extraClasses += 'border border-warning/50' } return ( @@ -86,7 +87,7 @@ function WidgetItemComponent({
{widget.isNew && ( - + جدید )} @@ -96,10 +97,13 @@ function WidgetItemComponent({ )} {isDisabled && ( - غیرفعال + غیرفعال )} {isSoon && ( - به زودی + به زودی + )} + {isBeta && ( + آزمایشی )}
diff --git a/src/layouts/widgets/yadkar/yadkar.tsx b/src/layouts/widgets/yadkar/yadkar.tsx index 059eaaf2..b397828a 100644 --- a/src/layouts/widgets/yadkar/yadkar.tsx +++ b/src/layouts/widgets/yadkar/yadkar.tsx @@ -10,6 +10,8 @@ import { } from 'react-icons/hi2' import { WidgetContainer } from '../widget-container' import { HabitsContent } from '../habit/habits.layout' +import { getFromStorage, setToStorage } from '@/common/storage' +import { useEffect } from 'react' type Tab = 'todos' | 'notes' | 'rabbit' export function YadkarWidget() { @@ -18,8 +20,20 @@ export function YadkarWidget() { const onChangeTab = (newTab: Tab) => { setTab(newTab) Analytics.event('yadkar_change_tab') + setToStorage('yadkar_tab', newTab) } + useEffect(() => { + const load = async () => { + const currentTab = await getFromStorage('yadkar_tab') + if (currentTab) { + setTab(currentTab as Tab) + } + } + + load() + }, []) + return (
@@ -42,7 +56,7 @@ export function YadkarWidget() { }, { id: 'rabbit', - label: 'عادت‌ها', + label: 'عادت‌ها (بتا)', icon: , }, ]} From 2b9e0a893c1f8ece1b3fc52a6a40d7909f881f3f Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 22:51:58 +0330 Subject: [PATCH 12/17] add habit skeleton --- .../habit/components/habit-item.skeleton.tsx | 22 ++++++++++++++ src/layouts/widgets/habit/habits.layout.tsx | 30 ++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 src/layouts/widgets/habit/components/habit-item.skeleton.tsx diff --git a/src/layouts/widgets/habit/components/habit-item.skeleton.tsx b/src/layouts/widgets/habit/components/habit-item.skeleton.tsx new file mode 100644 index 00000000..780947e7 --- /dev/null +++ b/src/layouts/widgets/habit/components/habit-item.skeleton.tsx @@ -0,0 +1,22 @@ +export function HabitItemSkeleton() { + return ( +
+
+
+ +
+
+
+
+ +
+
+ +
+ {Array.from({ length: 7 }).map((_, index) => ( +
+ ))} +
+
+ ) +} diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx index 3a017bbd..c083ca72 100644 --- a/src/layouts/widgets/habit/habits.layout.tsx +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -17,6 +17,8 @@ import { HabitDetailModal } from './components/habit-detail.modal' import { HabitFormModal } from './components/habit-form.modal' import { HabitItem } from './components/habit.item' import { BiPlus } from 'react-icons/bi' +import { callEvent } from '@/common/utils/call-event' +import { HabitItemSkeleton } from './components/habit-item.skeleton' export function HabitsContent() { const { isAuthenticated } = useAuth() @@ -37,12 +39,21 @@ export function HabitsContent() { } const handleAddHabit = () => { + if (!isAuthenticated) { + callEvent('openProfile') + return + } setEditingHabit(null) setShowForm(true) Analytics.event('habit_form_opened') } const handleEditHabit = (habit: Habit) => { + if (!isAuthenticated) { + callEvent('openProfile') + return + } + setEditingHabit(habit) setShowForm(true) Analytics.event('habit_edit_opened') @@ -65,6 +76,11 @@ export function HabitsContent() { } const onRefresh = () => { + if (!isAuthenticated) { + callEvent('openProfile') + return + } + refetch() Analytics.event('habit_refetch') } @@ -83,9 +99,6 @@ export function HabitsContent() {

عادت‌ها

- {/* - آزمایشی - */}
@@ -111,13 +124,14 @@ export function HabitsContent() {
+ {isRefetching ? ( + + ) : null} + {isLoading ? (
- {[...Array(3)].map((_, i) => ( -
+ {[...Array(4)].map((_, i) => ( + ))}
) : data?.items?.length === 0 ? ( From 95f3ef45ce76b1a805c6b522474af081ff05c89b Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 23:06:06 +0330 Subject: [PATCH 13/17] add font cache --- background/cache.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/background/cache.ts b/background/cache.ts index 2bc9171c..69ef8f12 100644 --- a/background/cache.ts +++ b/background/cache.ts @@ -51,6 +51,24 @@ export function setupCaching() { const isDev = import.meta.env.DEV if (!isDev) { + registerRoute( + ({ url }) => + url.href === 'https://cdn.widgetify.ir/fonts/remote-fonts.css', + new StaleWhileRevalidate({ + cacheName: 'remote-fonts-css-cache', + plugins: [ + new CacheableResponsePlugin({ + statuses: [0, 200], + }), + new ExpirationPlugin({ + maxEntries: 1, + maxAgeSeconds: 2 * 60, + purgeOnQuotaError: true, + }), + ], + }) + ) + registerRoute( ({ request }) => request.destination === 'script' || request.destination === 'style', From e79b926c94387b66698092692afe637f0eb44bec Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 23:06:52 +0330 Subject: [PATCH 14/17] remove logs --- .../setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx | 1 - src/pages/home/home.page.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx b/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx index 68ea808d..2f72312c 100644 --- a/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx +++ b/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx @@ -60,7 +60,6 @@ function applyWallpaper(wallpaper: StoredWallpaper) { }) video.onerror = () => { - console.error('Video error:', video.error) document.body.style.backgroundColor = '#000' } video.onloadeddata = () => { diff --git a/src/pages/home/home.page.tsx b/src/pages/home/home.page.tsx index 6c5b9987..bf636c12 100644 --- a/src/pages/home/home.page.tsx +++ b/src/pages/home/home.page.tsx @@ -81,7 +81,6 @@ export function HomePage() { } function onDoneTour(data: any) { - console.log(data) if ( data.status === 'finished' || data.status === 'skipped' || From 5942a78cf198f4ccc072b067cda637d31e926179 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 23:12:34 +0330 Subject: [PATCH 15/17] improve no items --- src/layouts/widgets/habit/habits.layout.tsx | 8 ++++++-- src/layouts/widgets/notes/notes.layout.tsx | 9 +++++++-- src/layouts/widgets/todos/todos.tsx | 13 ++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/layouts/widgets/habit/habits.layout.tsx b/src/layouts/widgets/habit/habits.layout.tsx index c083ca72..279343a8 100644 --- a/src/layouts/widgets/habit/habits.layout.tsx +++ b/src/layouts/widgets/habit/habits.layout.tsx @@ -136,8 +136,12 @@ export function HabitsContent() {
) : data?.items?.length === 0 ? (
-
- 🎯 +
+ بدون عادت

هیچ عادتی برای نمایش وجود ندارد diff --git a/src/layouts/widgets/notes/notes.layout.tsx b/src/layouts/widgets/notes/notes.layout.tsx index bd742254..b2d9180d 100644 --- a/src/layouts/widgets/notes/notes.layout.tsx +++ b/src/layouts/widgets/notes/notes.layout.tsx @@ -1,4 +1,3 @@ -import { PiNotepad } from 'react-icons/pi' import Analytics from '@/analytics' import { useGeneralSetting } from '@/context/general-setting.context' import { NotesProvider, useNotes } from '@/context/notes.context' @@ -17,7 +16,13 @@ function NotesContent() {

- +
+ بدون عادت +

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

منتظر چی هستی؟ شروع کن به نوشتن! diff --git a/src/layouts/widgets/todos/todos.tsx b/src/layouts/widgets/todos/todos.tsx index d3485cf7..092da2e9 100644 --- a/src/layouts/widgets/todos/todos.tsx +++ b/src/layouts/widgets/todos/todos.tsx @@ -1,5 +1,4 @@ import { useState } from 'react' -import { FiList } from 'react-icons/fi' import { ExpandableTodoInput } from './expandable-todo-input' import { useAuth } from '@/context/auth.context' import Analytics from '@/analytics' @@ -311,12 +310,12 @@ function TodosEmpty() { 'flex-1 flex flex-col items-center justify-center gap-y-1.5 px-5 py-8' } > -
- +
+ بدون عادت

هیچ تسکی برای نمایش وجود ندارد From ec403ee1305f849de8c58e1c93fb5ab020b79ee3 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Jun 2026 23:38:49 +0330 Subject: [PATCH 16/17] feat(habit-form): collapse advanced fields into expandable section --- .../habit/components/habit-form.modal.tsx | 303 ++++++++++-------- wxt.config.ts | 2 +- 2 files changed, 178 insertions(+), 127 deletions(-) diff --git a/src/layouts/widgets/habit/components/habit-form.modal.tsx b/src/layouts/widgets/habit/components/habit-form.modal.tsx index fed38619..6ad0dc65 100644 --- a/src/layouts/widgets/habit/components/habit-form.modal.tsx +++ b/src/layouts/widgets/habit/components/habit-form.modal.tsx @@ -24,8 +24,9 @@ import { import { useUpdateHabit } from '@/services/hooks/habit/update-habit.hook' import { translateError } from '@/utils/translate-error' import Tooltip from '@/components/toolTip' -import { BiInfoCircle } from 'react-icons/bi' +import { BiChevronDown, BiInfoCircle } from 'react-icons/bi' import type { HabitIcon } from '@/services/hooks/habit/get-habits.hook' +import { AnimatePresence, motion } from 'framer-motion' interface HabitFormModalProps { isOpen: boolean @@ -56,6 +57,7 @@ export function HabitFormModal({ colors, }: HabitFormModalProps) { const isEdit = !!habit + const [showAdvanced, setShowAdvanced] = useState(false) const [form, setForm] = useState(defaultForm) const { mutateAsync: addHabit, isPending: isAdding } = useAddHabit() const { mutateAsync: updateHabit, isPending: isUpdating } = useUpdateHabit() @@ -145,7 +147,7 @@ export function HabitFormModal({

-
+
{icons.map((emoji) => (
-
-
-
-
- - - - -
- - updateField('unit', value as HabitUnit) - } - className="!w-full h-8!" - /> -
-
-
- - - - -
- - updateField('comparison', value as HabitComparison) - } - className="!w-full h-8!" - /> -
-
+
+ - {form.unit === HabitUnit.CUSTOM && ( -
- updateField('customUnit', value)} - placeholder="نام واحد (مثلا: کیلومتر)" - direction="rtl" - className="h-8!" - /> -
- )} + + {showAdvanced && ( + +
+
+
+
+ + + + +
+ + updateField( + 'unit', + value as HabitUnit + ) + } + className="w-full! h-8!" + /> +
+
+
+ + + + +
+ + updateField( + 'comparison', + value as HabitComparison + ) + } + className="w-full! h-8!" + /> +
+
-
-
-
- - - - -
- - updateField('target', Number(value) || 0) - } - /> -
-
-
- - - - -
- - updateField('frequency', value as HabitFrequency) - } - className="!w-full h-8!" - /> -
-
+ {form.unit === HabitUnit.CUSTOM && ( +
+ + updateField('customUnit', value) + } + placeholder="نام واحد (مثلا: کیلومتر)" + direction="rtl" + className="h-8!" + /> +
+ )} - {form.frequency !== HabitFrequency.DAILY && ( -
-
- - - - -
- - updateField('frequencyCount', Number(value) || 1) - } - className="h-8!" - /> -
- )} +
+
+
+ + + + +
+ + updateField( + 'target', + Number(value) || 0 + ) + } + /> +
+
+
+ + + + +
+ + updateField( + 'frequency', + value as HabitFrequency + ) + } + className="w-full! h-8!" + /> +
+
+ + {form.frequency !== HabitFrequency.DAILY && ( +
+
+ + + + +
+ + updateField( + 'frequencyCount', + Number(value) || 1 + ) + } + className="h-8!" + /> +
+ )} +
+
+ )} +