diff --git a/package-lock.json b/package-lock.json
index 867b7de..111c915 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,7 @@
"jsencrypt": "^3.5.4",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
+ "react-day-picker": "^10.0.1",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.4",
"react-icons": "^5.6.0",
@@ -392,6 +393,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@date-fns/tz": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz",
+ "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==",
+ "license": "MIT"
+ },
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@@ -4564,6 +4571,16 @@
"node": ">=12"
}
},
+ "node_modules/date-fns": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
+ "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -6870,6 +6887,32 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-day-picker": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.1.tgz",
+ "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@date-fns/tz": "^1.4.1",
+ "date-fns": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/gpbl"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
diff --git a/package.json b/package.json
index c3c0f2a..f9298a5 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"jsencrypt": "^3.5.4",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
+ "react-day-picker": "^10.0.1",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.4",
"react-icons": "^5.6.0",
diff --git a/src/components/DateField.tsx b/src/components/DateField.tsx
new file mode 100644
index 0000000..d634c2d
--- /dev/null
+++ b/src/components/DateField.tsx
@@ -0,0 +1,106 @@
+import * as Popover from '@radix-ui/react-popover';
+import { enUS, faIR, ru, zhCN } from 'date-fns/locale';
+import { useState } from 'react';
+import { DayPicker, type Matcher } from 'react-day-picker';
+import { useTranslation } from 'react-i18next';
+
+import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from './icons';
+
+import 'react-day-picker/style.css';
+
+interface DateFieldProps {
+ /** Selected date as 'YYYY-MM-DD', or '' when empty. */
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ /** Inclusive bounds as 'YYYY-MM-DD'. */
+ min?: string;
+ max?: string;
+ /** Override the trigger button classes. */
+ className?: string;
+}
+
+const LOCALES = { ru, en: enUS, zh: zhCN, fa: faIR } as const;
+
+const pad = (n: number) => String(n).padStart(2, '0');
+const toISO = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+const parseISO = (s: string): Date | undefined => {
+ if (!s) return undefined;
+ const [y, m, d] = s.split('-').map(Number);
+ if (!y || !m || !d) return undefined;
+ return new Date(y, m - 1, d);
+};
+
+function NavChevron({ orientation }: { orientation?: 'left' | 'right' | 'up' | 'down' }) {
+ return orientation === 'right' ? (
+
+ ) : (
+
+ );
+}
+
+/**
+ * Dark-themed date field — Radix Popover trigger + react-day-picker calendar.
+ * Drop-in replacement for ``: takes/returns 'YYYY-MM-DD'.
+ * Month/weekday names follow the UI language (date-fns locale), navigation uses
+ * the shared barrel chevrons, theming via the .rdp-dark variable overrides.
+ */
+export function DateField({ value, onChange, placeholder, min, max, className }: DateFieldProps) {
+ const { i18n } = useTranslation();
+ const [open, setOpen] = useState(false);
+
+ const selected = parseISO(value);
+ const locale = LOCALES[i18n.language as keyof typeof LOCALES] ?? enUS;
+ const display = selected
+ ? selected.toLocaleDateString(i18n.language, {
+ day: '2-digit',
+ month: 'short',
+ year: 'numeric',
+ })
+ : '';
+
+ const disabled: Matcher[] = [];
+ const minDate = parseISO(min ?? '');
+ const maxDate = parseISO(max ?? '');
+ if (minDate) disabled.push({ before: minDate });
+ if (maxDate) disabled.push({ after: maxDate });
+
+ return (
+
+
+
+
+
+
+ {
+ if (d) {
+ onChange(toISO(d));
+ setOpen(false);
+ }
+ }}
+ />
+
+
+
+ );
+}
diff --git a/src/components/sales-stats/PeriodSelector.tsx b/src/components/sales-stats/PeriodSelector.tsx
index c55874a..19c465b 100644
--- a/src/components/sales-stats/PeriodSelector.tsx
+++ b/src/components/sales-stats/PeriodSelector.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { SALES_STATS } from '../../constants/salesStats';
import { getMonthToDateRange, isMonthToDate } from '../../utils/period';
+import { DateField } from '../DateField';
interface PeriodSelectorProps {
value: { days?: number; startDate?: string; endDate?: string };
@@ -68,18 +69,18 @@ export function PeriodSelector({ value, onChange }: PeriodSelectorProps) {
{showCustom && (
- handleDateChange('startDate', e.target.value)}
- className="rounded-lg border border-dark-600 bg-dark-800 px-2 py-1 text-sm text-dark-200"
+ onChange={(v) => handleDateChange('startDate', v)}
+ max={value.endDate}
+ placeholder={t('admin.salesStats.period.from')}
/>
{'—'}
- handleDateChange('endDate', e.target.value)}
- className="rounded-lg border border-dark-600 bg-dark-800 px-2 py-1 text-sm text-dark-200"
+ onChange={(v) => handleDateChange('endDate', v)}
+ min={value.startDate}
+ placeholder={t('admin.salesStats.period.to')}
/>
)}
diff --git a/src/locales/en.json b/src/locales/en.json
index c14df7d..d229697 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1283,6 +1283,8 @@
"subtitle": "Revenue, trials, subscriptions, and renewals",
"period": {
"thisMonth": "This month",
+ "from": "From",
+ "to": "To",
"week": "7 days",
"month": "30 days",
"quarter": "90 days",
diff --git a/src/locales/fa.json b/src/locales/fa.json
index f84cde8..7006b10 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -1156,6 +1156,8 @@
"subtitle": "درآمد، آزمایشی، اشتراک و تمدید",
"period": {
"thisMonth": "این ماه",
+ "from": "از",
+ "to": "تا",
"week": "۷ روز",
"month": "۳۰ روز",
"quarter": "۹۰ روز",
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 8c18e60..b0db7dd 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -1305,6 +1305,8 @@
"subtitle": "Доход, триалы, подписки и продления",
"period": {
"thisMonth": "Этот месяц",
+ "from": "Начало",
+ "to": "Конец",
"week": "7 дней",
"month": "30 дней",
"quarter": "90 дней",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 68c6184..7778363 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -1156,6 +1156,8 @@
"subtitle": "收入、试用、订阅和续订",
"period": {
"thisMonth": "本月",
+ "from": "从",
+ "to": "到",
"week": "7天",
"month": "30天",
"quarter": "90天",
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 1ed7da8..2561198 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1392,6 +1392,44 @@ input[type='checkbox']:hover:not(:checked) {
outline: none;
}
+/* react-day-picker (DateField) — dark theme via the library's CSS variables. */
+.rdp-dark {
+ --rdp-accent-color: rgb(var(--color-accent-500));
+ --rdp-accent-background-color: rgb(var(--color-accent-500) / 0.18);
+ --rdp-today-color: rgb(var(--color-accent-400));
+ --rdp-day-height: 2.25rem;
+ --rdp-day-width: 2.25rem;
+ --rdp-outside-opacity: 0.35;
+ --rdp-disabled-opacity: 0.3;
+ --rdp-selected-border: none;
+ color: rgb(var(--color-dark-200));
+ font-size: 0.85rem;
+}
+.rdp-dark .rdp-month_caption,
+.rdp-dark .rdp-caption_label {
+ color: rgb(var(--color-dark-100));
+ font-weight: 600;
+ text-transform: capitalize;
+}
+.rdp-dark .rdp-weekday {
+ color: rgb(var(--color-dark-500));
+ font-weight: 500;
+ text-transform: capitalize;
+}
+.rdp-dark .rdp-day_button {
+ border-radius: 0.5rem;
+}
+.rdp-dark .rdp-day_button:hover:not([disabled]) {
+ background-color: rgb(var(--color-dark-700));
+}
+.rdp-dark .rdp-selected .rdp-day_button {
+ color: #fff;
+ font-weight: 600;
+}
+.rdp-dark .rdp-chevron {
+ fill: currentColor;
+}
+
.light input[type='checkbox'] {
border-color: rgb(var(--color-champagne-400));
background-color: rgb(var(--color-champagne-100));