From d65b73a593a1b92d0057eb7af49aaf3fcd034ca9 Mon Sep 17 00:00:00 2001
From: AmirmahdiNourkazemi
Date: Mon, 23 Feb 2026 11:09:57 +0330
Subject: [PATCH] feat: add feature notification
---
app/admin/dashboard/page.tsx | 12 +-
app/admin/reminders/page.tsx | 247 ++++++++++++++++++++
components/admin/reminders/ReminderForm.tsx | 243 +++++++++++++++++++
components/admin/reminders/ReminderList.tsx | 113 +++++++++
lib/api/reminders.ts | 64 +++++
lib/utils.ts | 14 ++
next-env.d.ts | 2 +-
types/reminder.ts | 50 ++++
8 files changed, 743 insertions(+), 2 deletions(-)
create mode 100644 app/admin/reminders/page.tsx
create mode 100644 components/admin/reminders/ReminderForm.tsx
create mode 100644 components/admin/reminders/ReminderList.tsx
create mode 100644 lib/api/reminders.ts
create mode 100644 types/reminder.ts
diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx
index 35c7ca5..18167a3 100644
--- a/app/admin/dashboard/page.tsx
+++ b/app/admin/dashboard/page.tsx
@@ -2,7 +2,8 @@
import { useAuth } from '@/contexts/AuthContext';
import Link from 'next/link';
-import { UsersIcon, CubeIcon, TagIcon } from '@heroicons/react/24/outline';
+import { UsersIcon, CubeIcon, TagIcon , BellIcon } from '@heroicons/react/24/outline';
+// import { BellIcon } from 'lucide-react';
export default function Dashboard() {
const { user, logout } = useAuth();
@@ -57,6 +58,15 @@ export default function Dashboard() {
+
+
+
+
مدیریت یادآوریها
+
+ ایجاد و مدیریت یادآوریها و پیامهای انگیزشی
+
+
+
diff --git a/app/admin/reminders/page.tsx b/app/admin/reminders/page.tsx
new file mode 100644
index 0000000..a5b7016
--- /dev/null
+++ b/app/admin/reminders/page.tsx
@@ -0,0 +1,247 @@
+'use client';
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { useAuth } from '@/contexts/AuthContext';
+import { packagesApi } from '@/lib/api/packages';
+import { remindersApi } from '@/lib/api/reminders';
+import ReminderList from '@/components/admin/reminders/ReminderList';
+import ReminderForm from '@/components/admin/reminders/ReminderForm';
+import { PackageName } from '@/types/package';
+import { Reminder, CreateReminderData, UpdateReminderData } from '@/types/reminder';
+import { PlusIcon } from '@heroicons/react/24/outline';
+
+export default function RemindersPage() {
+ const { token } = useAuth();
+ const [packages, setPackages] = useState([]);
+ const [selectedPackage, setSelectedPackage] = useState(null);
+ const [reminders, setReminders] = useState([]);
+ const [selectedReminder, setSelectedReminder] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isLoadingPackages, setIsLoadingPackages] = useState(false);
+ const [isFormVisible, setIsFormVisible] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(null);
+
+ const initialLoadRef = useRef(false);
+ const prevPackageRef = useRef(null);
+
+ // Load packages on mount
+ useEffect(() => {
+ if (token && !initialLoadRef.current) {
+ initialLoadRef.current = true;
+ loadPackages();
+ }
+ }, [token]);
+
+ const loadPackages = async () => {
+ setIsLoadingPackages(true);
+ setError(null);
+ try {
+ const data = await packagesApi.getAllPackages(token!);
+ setPackages(data);
+ if (data.length > 0 && !selectedPackage) {
+ setSelectedPackage(data[0]);
+ }
+ } catch (err) {
+ setError('خطا در دریافت لیست پکیجها');
+ } finally {
+ setIsLoadingPackages(false);
+ }
+ };
+
+ const loadReminders = useCallback(async (packageName: string, force = false) => {
+ if (!force && prevPackageRef.current === packageName) {
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ prevPackageRef.current = packageName;
+
+ try {
+ const data = await remindersApi.getReminders(packageName, token!);
+ setReminders(data);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'خطا در دریافت یادآوریها');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [token]);
+
+ // Load reminders when package changes
+ useEffect(() => {
+ if (selectedPackage?.name && token) {
+ loadReminders(selectedPackage.name);
+ } else {
+ setReminders([]);
+ }
+ }, [selectedPackage?.name, token, loadReminders]);
+
+ const handlePackageChange = (e: React.ChangeEvent) => {
+ const pkg = packages.find(p => p.name === e.target.value);
+ setSelectedPackage(pkg || null);
+ setIsFormVisible(false);
+ setSelectedReminder(null);
+ setError(null);
+ setSuccess(null);
+ };
+
+ const handleCreate = () => {
+ setSelectedReminder(null);
+ setIsFormVisible(true);
+ };
+
+ const handleEdit = (reminder: Reminder) => {
+ setSelectedReminder(reminder);
+ setIsFormVisible(true);
+ };
+
+ const handleDelete = async (reminder: Reminder) => {
+ if (!selectedPackage) return;
+
+ if (!confirm(`آیا از حذف یادآوری "${reminder.title}" اطمینان دارید؟`)) {
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ setSuccess(null);
+
+ try {
+ await remindersApi.deleteReminder(selectedPackage.name!, reminder.id, token!);
+ setSuccess('یادآوری با موفقیت حذف شد');
+ if (selectedPackage.name) {
+ prevPackageRef.current = null;
+ await loadReminders(selectedPackage.name, true);
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'خطا در حذف یادآوری');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSubmit = async (data: CreateReminderData | UpdateReminderData) => {
+ if (!selectedPackage) return;
+
+ setIsLoading(true);
+ setError(null);
+ setSuccess(null);
+
+ try {
+ if (selectedReminder) {
+ // Update existing reminder
+ await remindersApi.updateReminder(selectedPackage.name!, selectedReminder.id, data, token!);
+ setSuccess('یادآوری با موفقیت بهروزرسانی شد');
+ } else {
+ // Create new reminder
+ await remindersApi.createReminder(selectedPackage.name!, data as CreateReminderData, token!);
+ setSuccess('یادآوری با موفقیت ایجاد شد');
+ }
+
+ if (selectedPackage.name) {
+ prevPackageRef.current = null;
+ await loadReminders(selectedPackage.name, true);
+ }
+
+ setIsFormVisible(false);
+ setSelectedReminder(null);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'خطا در ذخیره یادآوری');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setIsFormVisible(false);
+ setSelectedReminder(null);
+ setError(null);
+ setSuccess(null);
+ };
+
+ return (
+
+
+
+
+ مدیریت یادآوریها
+
+
+
+ {/* Package Selector */}
+
+
+
+
+
+ {/* Messages */}
+ {error && (
+
+ )}
+
+ {success && (
+
+ )}
+
+ {/* Form or List */}
+ {selectedPackage && (
+ <>
+ {!isFormVisible && (
+
+ )}
+
+ {isFormVisible ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/admin/reminders/ReminderForm.tsx b/components/admin/reminders/ReminderForm.tsx
new file mode 100644
index 0000000..4e909ae
--- /dev/null
+++ b/components/admin/reminders/ReminderForm.tsx
@@ -0,0 +1,243 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Reminder, CreateReminderData, UpdateReminderData, REMINDER_TYPES } from '@/types/reminder';
+
+interface ReminderFormProps {
+ reminder?: Reminder | null;
+ packageName: string;
+ onSubmit: (data: CreateReminderData | UpdateReminderData) => Promise;
+ onCancel: () => void;
+ isLoading: boolean;
+}
+
+export default function ReminderForm({ reminder, packageName, onSubmit, onCancel, isLoading }: ReminderFormProps) {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ time: '',
+ repeatable: false,
+ repeat_days: null,
+ type: 'reminder',
+ });
+
+ useEffect(() => {
+ if (reminder) {
+ // Format datetime for input field (remove milliseconds and timezone)
+ // Convert from "2025-12-13T15:30:00.000" to "2025-12-13T15:30" for input
+ const formattedTime = reminder.time ? reminder.time.replace(/\.\d+/, '').slice(0, 16) : '';
+
+ setFormData({
+ title: reminder.title || '',
+ description: reminder.description || '',
+ time: formattedTime,
+ repeatable: reminder.repeatable || false,
+ repeat_days: reminder.repeat_days,
+ type: reminder.type || 'reminder',
+ });
+ } else {
+ // Set default time to current date + 1 hour
+ const defaultTime = new Date();
+ defaultTime.setHours(defaultTime.getHours() + 1);
+
+ // Format as YYYY-MM-DDTHH:MM for datetime-local input
+ const year = defaultTime.getFullYear();
+ const month = String(defaultTime.getMonth() + 1).padStart(2, '0');
+ const day = String(defaultTime.getDate()).padStart(2, '0');
+ const hours = String(defaultTime.getHours()).padStart(2, '0');
+ const minutes = String(defaultTime.getMinutes()).padStart(2, '0');
+
+ const formattedDefaultTime = `${year}-${month}-${day}T${hours}:${minutes}`;
+
+ setFormData({
+ title: '',
+ description: '',
+ time: formattedDefaultTime,
+ repeatable: false,
+ repeat_days: null,
+ type: 'reminder',
+ });
+ }
+ }, [reminder]);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value, type } = e.target;
+
+ if (type === 'checkbox') {
+ const checked = (e.target as HTMLInputElement).checked;
+ setFormData(prev => ({
+ ...prev,
+ [name]: checked,
+ }));
+ } else {
+ setFormData(prev => ({
+ ...prev,
+ [name]: value,
+ }));
+ }
+ };
+
+ const handleNumberChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value ? parseInt(value, 10) : null,
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Format the time properly for API: "2025-12-13 20:00:00.000"
+ let formattedTime = '';
+
+ if (formData.time) {
+ // Input comes as "2025-12-13T20:00" from datetime-local
+ // Replace 'T' with space and add milliseconds
+ formattedTime = formData.time.replace('T', ' ') + ':00.000';
+ }
+
+ const submitData = {
+ ...formData,
+ time: formattedTime,
+ };
+
+ await onSubmit(submitData);
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/admin/reminders/ReminderList.tsx b/components/admin/reminders/ReminderList.tsx
new file mode 100644
index 0000000..1a93c2c
--- /dev/null
+++ b/components/admin/reminders/ReminderList.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { Reminder } from '@/types/reminder';
+import { getReminderTypeLabel } from '@/types/reminder';
+import { PencilIcon, TrashIcon, BellIcon, SparklesIcon } from '@heroicons/react/24/outline';
+import { formatDate, formatTime } from '@/lib/utils';
+
+interface ReminderListProps {
+ reminders: Reminder[];
+ packageName: string;
+ onEdit: (reminder: Reminder) => void;
+ onDelete: (reminder: Reminder) => void;
+ isLoading: boolean;
+}
+
+export default function ReminderList({ reminders, packageName, onEdit, onDelete, isLoading }: ReminderListProps) {
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!reminders || reminders.length === 0) {
+ return (
+
+
هیچ یادآوری برای این پکیج یافت نشد
+
+ );
+ }
+
+ // Sort reminders by time (most recent first)
+ const sortedReminders = [...reminders].sort((a, b) =>
+ new Date(a.time).getTime() - new Date(b.time).getTime()
+ );
+
+ return (
+
+
+
+ یادآوریهای پکیج {packageName}
+
+
+
+ {sortedReminders.map((reminder) => (
+ -
+
+
+
+
+ {reminder.type === 'motivate' ? (
+
+ ) : (
+
+ )}
+
+ {reminder.title}
+
+
+
+ {getReminderTypeLabel(reminder.type)}
+
+
+
+ {reminder.description && (
+
+ {reminder.description}
+
+ )}
+
+
+
+ زمان: {formatDate(reminder.time)} {formatTime(reminder.time)}
+
+
+
+ تکرار: {reminder.repeatable ? 'فعال' : 'غیرفعال'}
+
+
+ {reminder.repeatable && reminder.repeat_days && (
+
+ فاصله تکرار: هر {reminder.repeat_days} روز
+
+ )}
+
+
+
+ آخرین بهروزرسانی: {formatDate(reminder.updated_at)}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/lib/api/reminders.ts b/lib/api/reminders.ts
new file mode 100644
index 0000000..a419a8d
--- /dev/null
+++ b/lib/api/reminders.ts
@@ -0,0 +1,64 @@
+import { apiClient } from './client';
+import { Reminder, CreateReminderData, UpdateReminderData, ApiResponse } from '@/types/reminder';
+
+export const remindersApi = {
+ // Get all reminders for a package
+ getReminders: async (packageName: string, token: string): Promise => {
+ return apiClient.get(`/package-names/${packageName}/reminders`, token);
+ },
+
+ // Get single reminder
+ getReminder: async (packageName: string, reminderId: number, token: string): Promise => {
+ return apiClient.get(`/package-names/${packageName}/reminders/${reminderId}`, token);
+ },
+
+ // Create new reminder
+ createReminder: async (packageName: string, data: CreateReminderData, token: string): Promise> => {
+ const formData = new FormData();
+
+ // Append all fields to FormData
+ Object.entries(data).forEach(([key, value]) => {
+ if (value !== null && value !== undefined) {
+ if (key === 'data' && typeof value === 'object') {
+ formData.append(key, JSON.stringify(value));
+ } else {
+ formData.append(key, String(value));
+ }
+ }
+ });
+
+ return apiClient.post>(`/package-names/${packageName}/reminders`, formData, token);
+ },
+
+ // Update reminder (using PUT)
+ updateReminder: async (packageName: string, reminderId: number, data: UpdateReminderData, token: string): Promise> => {
+ return apiClient.put>(
+ `/package-names/${packageName}/reminders/${reminderId}`,
+ data,
+ token
+ );
+ },
+
+ // Delete reminder
+ deleteReminder: async (packageName: string, reminderId: number, token: string): Promise> => {
+ return apiClient.delete>(`/package-names/${packageName}/reminders/${reminderId}`, undefined, token);
+ },
+
+ // Alternative update method using POST with _method spoofing (if PUT is not supported)
+ updateReminderWithSpoof: async (packageName: string, reminderId: number, data: UpdateReminderData, token: string): Promise> => {
+ const formData = new FormData();
+ formData.append('_method', 'PUT');
+
+ Object.entries(data).forEach(([key, value]) => {
+ if (value !== null && value !== undefined) {
+ if (key === 'data' && typeof value === 'object') {
+ formData.append(key, JSON.stringify(value));
+ } else {
+ formData.append(key, String(value));
+ }
+ }
+ });
+
+ return apiClient.post>(`/package-names/${packageName}/reminders/${reminderId}`, formData, token);
+ }
+};
\ No newline at end of file
diff --git a/lib/utils.ts b/lib/utils.ts
index 788f29c..7a031f2 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -49,4 +49,18 @@ export const getEmail = (email: string | null): string => {
export const getMobile = (mobile: string | null): string => {
return mobile || 'شماره موبایل ثبت نشده';
+};
+
+export const formatTime = (dateString: string | null): string => {
+ if (!dateString) return '';
+
+ try {
+ const date = new Date(dateString);
+ return new Intl.DateTimeFormat('fa-IR', {
+ hour: '2-digit',
+ minute: '2-digit',
+ }).format(date);
+ } catch {
+ return '';
+ }
};
\ No newline at end of file
diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/types/reminder.ts b/types/reminder.ts
new file mode 100644
index 0000000..e748d08
--- /dev/null
+++ b/types/reminder.ts
@@ -0,0 +1,50 @@
+export type ReminderType = 'reminder' | 'motivate';
+
+export const REMINDER_TYPES = {
+ reminder: 'یادآوری',
+ motivate: 'انگیزشی'
+} as const;
+
+export interface Reminder {
+ id: number;
+ title: string;
+ description: string | null;
+ time: string;
+ repeatable: boolean;
+ repeat_days: number | null;
+ data: any | null;
+ type: ReminderType;
+ package_name?: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface CreateReminderData {
+ title: string;
+ description?: string | null;
+ time: string;
+ repeatable?: boolean;
+ repeat_days?: number | null;
+ data?: any | null;
+ type: ReminderType;
+}
+
+export interface UpdateReminderData {
+ title?: string;
+ description?: string | null;
+ time?: string;
+ repeatable?: boolean;
+ repeat_days?: number | null;
+ data?: any | null;
+ type?: ReminderType;
+}
+
+export interface ApiResponse {
+ data: T;
+ message?: string;
+ status?: number;
+}
+
+export const getReminderTypeLabel = (type: ReminderType): string => {
+ return REMINDER_TYPES[type] || type;
+};
\ No newline at end of file