First commit

This commit is contained in:
2026-02-19 01:15:36 +03:30
commit a898eccbff
1216 changed files with 189771 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false })
export default Chart
+5
View File
@@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const ReactPlayer = dynamic(() => import('react-player'), { ssr: false })
export default ReactPlayer
+4
View File
@@ -0,0 +1,4 @@
'use client'
// Third-party imports
export * from 'recharts'
+170
View File
@@ -0,0 +1,170 @@
/**
* API Client for communicating with Backend via Envoy Gateway
*/
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || process.env.ENVOY_GATEWAY_URL || 'http://localhost:9035'
export interface ApiError {
message: string
code?: number
details?: any
}
export class ApiClient {
private baseURL: string
private defaultHeaders: Record<string, string>
constructor(baseURL: string = API_BASE_URL) {
this.baseURL = baseURL.replace(/\/$/, '') // Remove trailing slash
this.defaultHeaders = {
'Content-Type': 'application/json',
}
}
/**
* Get authorization token from localStorage
*/
private getAuthToken(): string | null {
if (typeof window === 'undefined') return null
return localStorage.getItem('auth_token')
}
/**
* Set authorization token in localStorage
*/
setAuthToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token)
}
}
/**
* Clear authorization token
*/
clearAuthToken(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('auth_token')
}
}
/**
* Get headers with authentication token
*/
private getHeaders(customHeaders?: Record<string, string>): Record<string, string> {
const headers = { ...this.defaultHeaders, ...customHeaders }
const token = this.getAuthToken()
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
return headers
}
/**
* Handle API response
*/
private async handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
let errorData: any
try {
errorData = await response.json()
} catch {
errorData = { message: response.statusText }
}
const error: ApiError = {
message: errorData.msg || errorData.message || 'An error occurred',
code: errorData.code || response.status,
details: errorData
}
// If unauthorized, clear token
if (response.status === 401) {
this.clearAuthToken()
}
throw error
}
// Handle empty responses
const contentType = response.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
return {} as T
}
return response.json()
}
/**
* GET request
*/
async get<T>(endpoint: string, customHeaders?: Record<string, string>): Promise<T> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
method: 'GET',
headers: this.getHeaders(customHeaders),
})
return this.handleResponse<T>(response)
}
/**
* POST request
*/
async post<T>(endpoint: string, data?: any, customHeaders?: Record<string, string>): Promise<T> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
method: 'POST',
headers: this.getHeaders(customHeaders),
body: data ? JSON.stringify(data) : undefined,
})
return this.handleResponse<T>(response)
}
/**
* PUT request
*/
async put<T>(endpoint: string, data?: any, customHeaders?: Record<string, string>): Promise<T> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
method: 'PUT',
headers: this.getHeaders(customHeaders),
body: data ? JSON.stringify(data) : undefined,
})
return this.handleResponse<T>(response)
}
/**
* PATCH request
*/
async patch<T>(endpoint: string, data?: any, customHeaders?: Record<string, string>): Promise<T> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
method: 'PATCH',
headers: this.getHeaders(customHeaders),
body: data ? JSON.stringify(data) : undefined,
})
return this.handleResponse<T>(response)
}
/**
* DELETE request
*/
async delete<T>(endpoint: string, customHeaders?: Record<string, string>): Promise<T> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
method: 'DELETE',
headers: this.getHeaders(customHeaders),
})
return this.handleResponse<T>(response)
}
}
// Export singleton instance
export const apiClient = new ApiClient()
+17
View File
@@ -0,0 +1,17 @@
/**
* API Services Export
*/
export * from './client'
export * from './types'
export * from './services/authService'
export * from './services/taskService'
export * from './services/eventService'
export * from './services/simulatorService'
export * from './services/chatService'
export * from './services/aiChatService'
export * from './services/kanbanService'
export * from './services/todoService'
export * from './services/userManagementService'
export * from './services/rolesPermissionsService'
+117
View File
@@ -0,0 +1,117 @@
/**
* AI Chat Service
* Handles AI chat-related API calls
*/
import { apiClient } from '../client'
import type { ChatMessage, ChatFile } from '@/types/apps/aiChatTypes'
export interface ReferencedTask {
taskId: string
source: 'calendar' | 'kanban' | 'todo'
title: string
}
export interface SendAIMessageRequest {
content: string
images?: string[]
files?: ChatFile[]
model: 'gpt-4' | 'gpt-3.5' | 'claude' | 'gemini'
conversationId?: string
}
export interface SendAIMessageResponse {
message: ChatMessage & {
id: string
isSensitive?: boolean
sensitiveReason?: string
}
assistantResponse: {
id: string
role: 'assistant'
content: string
timestamp: string
referencedTasks?: ReferencedTask[]
}
}
export interface Conversation {
id: string
title: string
lastMessage: string
timestamp: string
messageCount: number
}
export interface GetAIConversationsResponse {
conversations: Conversation[]
}
export interface GetAIMessagesResponse {
messages: (ChatMessage & {
id: string
timestamp: string
})[]
}
export interface AIChatTask {
id: string
title: string
description: string
deadline: number
routine: 0 | 1 | 2 | 3 | 4
tags: string[]
source: 'calendar' | 'kanban' | 'todo'
status: string
priority: 'high' | 'medium' | 'low'
}
export interface GetAIChatTasksResponse {
tasks: AIChatTask[]
}
export const aiChatService = {
/**
* ارسال پیام به AI
*/
async sendMessage(data: SendAIMessageRequest): Promise<SendAIMessageResponse> {
return apiClient.post<SendAIMessageResponse>('/api/ai-chat/messages', data)
},
/**
* دریافت تاریخچه مکالمه
*/
async getConversationMessages(conversationId: string): Promise<(ChatMessage & { id: string; timestamp: string })[]> {
const response = await apiClient.get<GetAIMessagesResponse>(
`/api/ai-chat/conversations/${conversationId}/messages`
)
return response.messages || []
},
/**
* دریافت لیست مکالمات
*/
async getConversations(): Promise<Conversation[]> {
const response = await apiClient.get<GetAIConversationsResponse>('/api/ai-chat/conversations')
return response.conversations || []
},
/**
* دریافت تسک‌های مرتبط (برای AI Chat)
*/
async getTasks(source?: 'calendar' | 'kanban' | 'todo', routine?: 0 | 1 | 2 | 3 | 4): Promise<AIChatTask[]> {
const params = new URLSearchParams()
if (source) params.append('source', source)
if (routine !== undefined) params.append('routine', routine.toString())
const queryString = params.toString()
const endpoint = `/api/ai-chat/tasks${queryString ? `?${queryString}` : ''}`
const response = await apiClient.get<GetAIChatTasksResponse>(endpoint)
return response.tasks || []
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Authentication Service
* Handles OTP-based authentication with the backend
*/
import { apiClient } from '../client'
export interface RequestOTPRequest {
phone_number: string
}
export interface RequestOTPResponse {
code: number
msg: string
token: string
}
export interface VerifyOTPRequest {
token: string
otp_code: string
}
export interface AuthUser {
id: number
username: string
email: string
first_name: string
last_name: string
phone_number: string
}
export interface VerifyOTPResponse {
code: number
msg: string
data: AuthUser
token: string
}
export interface UpdateProfilePayload {
first_name: string
last_name: string
email: string
}
export interface UpdateProfileResponse {
code: number
msg: string
data: AuthUser
}
export const authService = {
/**
* Request OTP for phone number authentication
*/
async requestOTP(phoneNumber: string): Promise<RequestOTPResponse> {
return apiClient.post<RequestOTPResponse>('/api/auth/request-otp/', {
phone_number: phoneNumber
})
},
/**
* Verify OTP code and get JWT token
*/
async verifyOTP(token: string, otpCode: string): Promise<VerifyOTPResponse> {
const response = await apiClient.post<VerifyOTPResponse>('/api/auth/verify-otp/', {
token,
otp_code: otpCode
})
if ( response.token) {
apiClient.setAuthToken(response.token)
}
return response
},
/**
* Update user profile (first name, last name, email)
*/
async updateProfile(payload: UpdateProfilePayload): Promise<UpdateProfileResponse> {
const response = await apiClient.patch<UpdateProfileResponse>('/users/me', payload)
if (response.code !== 200 || !response.data) {
throw new Error(response.msg || 'Failed to update profile')
}
return response
},
/**
* Logout - clear authentication token
*/
logout(): void {
apiClient.clearAuthToken()
}
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Chat Service
* Handles chat-related API calls
*/
import { apiClient } from '../client'
import type { ContactType, ChatType, ProfileUserType, UserChatType } from '@/types/apps/chatTypes'
export interface GetContactsResponse {
contacts: ContactType[]
}
export interface GetChatConversationsResponse {
chats: ChatType[]
}
export interface GetChatMessagesResponse {
messages: UserChatType[]
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
export interface SendChatMessageRequest {
message: string
}
export interface SendChatMessageResponse {
message: UserChatType & {
id: number
}
}
export interface MarkMessageAsReadResponse {
success: boolean
}
export interface GetProfileResponse {
profileUser: ProfileUserType
}
export const chatService = {
/**
* دریافت لیست مخاطبین
*/
async getContacts(): Promise<ContactType[]> {
const response = await apiClient.get<GetContactsResponse>('/api/chat/contacts')
return response.contacts || []
},
/**
* دریافت چت‌های کاربر
*/
async getConversations(): Promise<ChatType[]> {
const response = await apiClient.get<GetChatConversationsResponse>('/api/chat/conversations')
return response.chats || []
},
/**
* دریافت پیام‌های چت
*/
async getMessages(
conversationId: number,
page?: number,
limit?: number
): Promise<{ messages: UserChatType[]; pagination: GetChatMessagesResponse['pagination'] }> {
const params = new URLSearchParams()
if (page !== undefined) params.append('page', page.toString())
if (limit !== undefined) params.append('limit', limit.toString())
const queryString = params.toString()
const endpoint = `/api/chat/conversations/${conversationId}/messages${queryString ? `?${queryString}` : ''}`
const response = await apiClient.get<GetChatMessagesResponse>(endpoint)
return {
messages: response.messages || [],
pagination: response.pagination
}
},
/**
* ارسال پیام جدید
*/
async sendMessage(conversationId: number, message: string): Promise<UserChatType & { id: number }> {
const response = await apiClient.post<SendChatMessageResponse>(
`/api/chat/conversations/${conversationId}/messages`,
{ message }
)
return response.message
},
/**
* به‌روزرسانی وضعیت مشاهده پیام
*/
async markMessageAsRead(messageId: number): Promise<boolean> {
const response = await apiClient.put<MarkMessageAsReadResponse>(`/api/chat/messages/${messageId}/read`)
return response.success
},
/**
* دریافت اطلاعات پروفایل کاربر
*/
async getProfile(): Promise<ProfileUserType> {
const response = await apiClient.get<GetProfileResponse>('/api/chat/profile')
return response.profileUser
}
}
+149
View File
@@ -0,0 +1,149 @@
/**
* Event Service
* Handles event-related API calls (calendar events)
*/
import { apiClient } from '../client'
import type { Author } from '../types'
export interface Event {
id: string
title: string
description: string
deadline: number // Unix timestamp
tags: string[]
author: Author
calendar: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
start: string // ISO 8601
end: string // ISO 8601
allDay: boolean
extendedProps?: Record<string, any>
}
export interface ListEventsParams {
start?: string // ISO 8601
end?: string // ISO 8601
calendar?: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
}
export interface CreateEventRequest {
title: string
description?: string
deadline?: number
tags?: string[]
calendar: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
start: string // ISO 8601
end: string // ISO 8601
allDay?: boolean
extendedProps?: Record<string, any>
}
export interface CreateEventResponse {
event: Event
}
export interface GetEventResponse {
event: Event
}
export interface ListEventsResponse {
events: Event[]
}
export interface UpdateEventRequest {
id: string
title?: string
description?: string
deadline?: number
tags?: string[]
calendar?: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
start?: string // ISO 8601
end?: string // ISO 8601
allDay?: boolean
extendedProps?: Record<string, any>
}
export interface UpdateEventResponse {
event: Event
}
export interface DeleteEventResponse {
success: boolean
}
export interface NonRoutineTask {
id: string
title: string
description: string
deadline: number
routine: 0
tags: string[]
author: Author
source: 'calendar' | 'kanban' | 'todo'
status: string
priority: 'high' | 'medium' | 'low'
}
export interface GetNonRoutineTasksResponse {
tasks: NonRoutineTask[]
}
export const eventService = {
/**
* Create a new event
*/
async createEvent(data: CreateEventRequest): Promise<Event> {
const response = await apiClient.post<CreateEventResponse>('/api/events', data)
return response.event
},
/**
* Get an event by ID
*/
async getEvent(id: string): Promise<Event> {
const response = await apiClient.get<GetEventResponse>(`/api/events/${id}`)
return response.event
},
/**
* List all events with optional filters
*/
async listEvents(params?: ListEventsParams): Promise<Event[]> {
const queryParams = new URLSearchParams()
if (params?.start) queryParams.append('start', params.start)
if (params?.end) queryParams.append('end', params.end)
if (params?.calendar) queryParams.append('calendar', params.calendar)
const queryString = queryParams.toString()
const endpoint = `/api/events${queryString ? `?${queryString}` : ''}`
const response = await apiClient.get<ListEventsResponse>(endpoint)
return response.events || []
},
/**
* Update an event
*/
async updateEvent(data: UpdateEventRequest): Promise<Event> {
const { id, ...updateData } = data
const response = await apiClient.put<UpdateEventResponse>(`/api/events/${id}`, updateData)
return response.event
},
/**
* Delete an event
*/
async deleteEvent(id: string): Promise<boolean> {
const response = await apiClient.delete<DeleteEventResponse>(`/api/events/${id}`)
return response.success
},
/**
* دریافت تسک‌های غیرروتین (مشترک با Kanban و Todo)
*/
async getNonRoutineTasks(): Promise<NonRoutineTask[]> {
const response = await apiClient.get<GetNonRoutineTasksResponse>('/api/tasks/non-routine')
return response.tasks || []
}
}
+290
View File
@@ -0,0 +1,290 @@
/**
* Kanban Service
* Handles kanban-related API calls
*/
import { apiClient } from '../client'
export interface Column {
id: number
title: string
taskIds: number[]
}
export interface AssignedUser {
src: string
name: string
}
export type TaskStatusType = 'pending' | 'in-progress' | 'completed'
export type TaskPriorityType = 'high' | 'medium' | 'low'
export interface KanbanTask {
id: number
title: string
badgeText?: string[]
attachments?: number
comments?: number
assigned?: AssignedUser[]
image?: string
dueDate?: string // ISO 8601
routine: 0 | 1 | 2 | 3 | 4
description?: string
tags?: string[]
priority?: TaskPriorityType
status?: TaskStatusType
source: 'kanban' | 'calendar' | 'todo'
}
export interface Board {
columns: Column[]
tasks: KanbanTask[]
}
export interface GetBoardResponse {
columns: Column[]
tasks: KanbanTask[]
}
export interface CreateColumnRequest {
title: string
}
export interface CreateColumnResponse {
column: Column
}
export interface UpdateColumnRequest {
title?: string
taskIds?: number[]
}
export interface UpdateColumnResponse {
column: Column
}
export interface DeleteColumnResponse {
success: boolean
}
export interface CreateKanbanTaskRequest {
columnId: number
title: string
description?: string
badgeText?: string[]
attachments?: number
comments?: number
assigned?: AssignedUser[]
image?: string
dueDate?: string // ISO 8601
routine?: 0 | 1 | 2 | 3 | 4
tags?: string[]
priority?: TaskPriorityType
status?: TaskStatusType
}
export interface CreateKanbanTaskResponse {
task: KanbanTask
}
export interface UpdateKanbanTaskRequest {
title?: string
description?: string
badgeText?: string[]
attachments?: number
comments?: number
assigned?: AssignedUser[]
image?: string
dueDate?: string
routine?: number
tags?: string[]
priority?: TaskPriorityType
status?: TaskStatusType
columnId?: number
}
export interface UpdateKanbanTaskResponse {
task: KanbanTask
}
export interface DeleteKanbanTaskResponse {
success: boolean
}
export interface MoveKanbanTaskRequest {
fromColumnId: number
toColumnId: number
position: number
}
export interface MoveKanbanTaskResponse {
success: boolean
columns: Column[]
}
// Helper functions for converting between frontend strings and backend enums
const statusToEnum = (status: TaskStatusType): number => {
const statusMap: Record<TaskStatusType, number> = {
'pending': 0,
'in-progress': 1,
'completed': 2
}
return statusMap[status] ?? 0
}
const enumToStatus = (enumValue: number): TaskStatusType => {
const statusMap: Record<number, TaskStatusType> = {
0: 'pending',
1: 'in-progress',
2: 'completed'
}
return statusMap[enumValue] ?? 'pending'
}
const priorityToEnum = (priority: TaskPriorityType): number => {
const priorityMap: Record<TaskPriorityType, number> = {
'high': 0,
'medium': 1,
'low': 2
}
return priorityMap[priority] ?? 1
}
const enumToPriority = (enumValue: number): TaskPriorityType => {
const priorityMap: Record<number, TaskPriorityType> = {
0: 'high',
1: 'medium',
2: 'low'
}
return priorityMap[enumValue] ?? 'medium'
}
// Convert backend Task response to frontend Task format
const convertBackendTaskToFrontend = (backendTask: any): KanbanTask => {
return {
id: backendTask.id,
title: backendTask.title,
badgeText: backendTask.badge_text || backendTask.badgeText,
attachments: backendTask.attachments,
comments: backendTask.comments,
assigned: backendTask.assigned,
image: backendTask.image,
dueDate: backendTask.due_date || backendTask.dueDate,
routine: (backendTask.routine ?? 0) as 0 | 1 | 2 | 3 | 4,
description: backendTask.description,
tags: backendTask.tags || [],
priority: backendTask.priority !== undefined ? enumToPriority(backendTask.priority) : undefined,
status: backendTask.status !== undefined ? enumToStatus(backendTask.status) : undefined,
source: backendTask.source === 0 ? 'kanban' : backendTask.source === 1 ? 'calendar' : 'todo'
}
}
export const kanbanService = {
/**
* دریافت Board Kanban
*/
async getBoard(): Promise<Board> {
const response = await apiClient.get<GetBoardResponse>('/api/kanban/board')
return {
columns: response.columns || [],
tasks: (response.tasks || []).map(convertBackendTaskToFrontend)
}
},
/**
* ایجاد ستون جدید
*/
async createColumn(title: string): Promise<Column> {
const response = await apiClient.post<CreateColumnResponse>('/api/kanban/columns', { title })
return response.column
},
/**
* به‌روزرسانی ستون
*/
async updateColumn(columnId: number, data: UpdateColumnRequest): Promise<Column> {
const response = await apiClient.put<UpdateColumnResponse>(`/api/kanban/columns/${columnId}`, data)
return response.column
},
/**
* حذف ستون
*/
async deleteColumn(columnId: number): Promise<boolean> {
const response = await apiClient.delete<DeleteColumnResponse>(`/api/kanban/columns/${columnId}`)
return response.success
},
/**
* ایجاد تسک جدید
*/
async createTask(data: CreateKanbanTaskRequest): Promise<KanbanTask> {
// Convert frontend format to backend format
const backendData: any = {
column_id: data.columnId,
title: data.title,
description: data.description,
badge_text: data.badgeText,
attachments: data.attachments,
comments: data.comments,
assigned: data.assigned,
image: data.image,
due_date: data.dueDate,
routine: data.routine ?? 0,
tags: data.tags || []
}
if (data.priority !== undefined) backendData.priority = priorityToEnum(data.priority)
if (data.status !== undefined) backendData.status = statusToEnum(data.status)
const response = await apiClient.post<CreateKanbanTaskResponse>('/api/kanban/tasks', backendData)
return convertBackendTaskToFrontend(response.task)
},
/**
* به‌روزرسانی تسک
*/
async updateTask(taskId: number, data: UpdateKanbanTaskRequest): Promise<KanbanTask> {
// Convert frontend format to backend format
const backendData: any = {}
if (data.title !== undefined) backendData.title = data.title
if (data.description !== undefined) backendData.description = data.description
if (data.badgeText !== undefined) backendData.badge_text = data.badgeText
if (data.attachments !== undefined) backendData.attachments = data.attachments
if (data.comments !== undefined) backendData.comments = data.comments
if (data.assigned !== undefined) backendData.assigned = data.assigned
if (data.image !== undefined) backendData.image = data.image
if (data.dueDate !== undefined) backendData.due_date = data.dueDate
if (data.routine !== undefined) backendData.routine = data.routine
if (data.tags !== undefined) backendData.tags = data.tags
if (data.priority !== undefined) backendData.priority = priorityToEnum(data.priority)
if (data.status !== undefined) backendData.status = statusToEnum(data.status)
if (data.columnId !== undefined) backendData.column_id = data.columnId
const response = await apiClient.put<UpdateKanbanTaskResponse>(`/api/kanban/tasks/${taskId}`, backendData)
return convertBackendTaskToFrontend(response.task)
},
/**
* حذف تسک
*/
async deleteTask(taskId: number): Promise<boolean> {
const response = await apiClient.delete<DeleteKanbanTaskResponse>(`/api/kanban/tasks/${taskId}`)
return response.success
},
/**
* جابجایی تسک بین ستون‌ها
*/
async moveTask(taskId: number, fromColumnId: number, toColumnId: number, position: number): Promise<Column[]> {
const response = await apiClient.put<MoveKanbanTaskResponse>(`/api/kanban/tasks/${taskId}/move`, {
fromColumnId,
toColumnId,
position
})
return response.columns || []
}
}
@@ -0,0 +1,194 @@
/**
* Roles & Permissions Service
* Handles roles and permissions-related API calls (Admin only)
*/
import { apiClient } from '../client'
export interface Role {
id: string
name: string
totalUsers: number
avatars: string[]
description: string
permissions?: string[]
}
export interface RoleStats {
administrator: number
author: number
editor: number
maintainer: number
subscriber: number
}
export interface GetRolesResponse {
roles: Role[]
roleStats: RoleStats
}
export interface RoleUser {
id: number
fullName: string
username: string
email: string
avatar: string
role: string
status: string
}
export interface GetRoleUsersResponse {
users: RoleUser[]
}
export interface CreateRoleRequest {
name: string
description: string
permissions: string[]
}
export interface CreateRoleResponse {
role: Role
}
export interface UpdateRoleRequest {
name?: string
description?: string
permissions?: string[]
}
export interface UpdateRoleResponse {
role: Role
}
export interface DeleteRoleResponse {
success: boolean
}
export interface Permission {
id: number
name: string
createdDate: string // ISO 8601
assignedTo: string | string[]
}
export interface GetPermissionsParams {
search?: string
}
export interface GetPermissionsResponse {
permissions: Permission[]
total: number
}
export interface CreatePermissionRequest {
name: string
assignedTo: string | string[]
}
export interface CreatePermissionResponse {
permission: Permission
}
export interface UpdatePermissionRequest {
name?: string
assignedTo?: string | string[]
}
export interface UpdatePermissionResponse {
permission: Permission
}
export interface DeletePermissionResponse {
success: boolean
}
export const rolesPermissionsService = {
/**
* دریافت لیست نقش‌ها
*/
async getRoles(): Promise<{ roles: Role[]; roleStats: RoleStats }> {
const response = await apiClient.get<GetRolesResponse>('/api/admin/roles')
return {
roles: response.roles || [],
roleStats: response.roleStats
}
},
/**
* دریافت کاربران بر اساس نقش
*/
async getRoleUsers(roleName: string): Promise<RoleUser[]> {
const response = await apiClient.get<GetRoleUsersResponse>(`/api/admin/roles/${roleName}/users`)
return response.users || []
},
/**
* ایجاد نقش جدید
*/
async createRole(data: CreateRoleRequest): Promise<Role> {
const response = await apiClient.post<CreateRoleResponse>('/api/admin/roles', data)
return response.role
},
/**
* به‌روزرسانی نقش
*/
async updateRole(roleId: string, data: UpdateRoleRequest): Promise<Role> {
const response = await apiClient.put<UpdateRoleResponse>(`/api/admin/roles/${roleId}`, data)
return response.role
},
/**
* حذف نقش
*/
async deleteRole(roleId: string): Promise<boolean> {
const response = await apiClient.delete<DeleteRoleResponse>(`/api/admin/roles/${roleId}`)
return response.success
},
/**
* دریافت لیست Permission ها
*/
async getPermissions(params?: GetPermissionsParams): Promise<{ permissions: Permission[]; total: number }> {
const queryParams = new URLSearchParams()
if (params?.search) queryParams.append('search', params.search)
const queryString = queryParams.toString()
const endpoint = `/api/admin/permissions${queryString ? `?${queryString}` : ''}`
const response = await apiClient.get<GetPermissionsResponse>(endpoint)
return {
permissions: response.permissions || [],
total: response.total || 0
}
},
/**
* ایجاد Permission جدید
*/
async createPermission(data: CreatePermissionRequest): Promise<Permission> {
const response = await apiClient.post<CreatePermissionResponse>('/api/admin/permissions', data)
return response.permission
},
/**
* به‌روزرسانی Permission
*/
async updatePermission(permissionId: number, data: UpdatePermissionRequest): Promise<Permission> {
const response = await apiClient.put<UpdatePermissionResponse>(`/api/admin/permissions/${permissionId}`, data)
return response.permission
},
/**
* حذف Permission
*/
async deletePermission(permissionId: number): Promise<boolean> {
const response = await apiClient.delete<DeletePermissionResponse>(`/api/admin/permissions/${permissionId}`)
return response.success
}
}
+93
View File
@@ -0,0 +1,93 @@
/**
* Simulator Service
* Handles simulator-related API calls
*/
import { apiClient } from '../client'
export interface Simulator {
id: string
file: string
type: string
description: string
title: string
}
export interface CreateSimulatorRequest {
file: string
type: string
description?: string
title?: string
}
export interface CreateSimulatorResponse {
simulator: Simulator
}
export interface GetSimulatorResponse {
simulator: Simulator
}
export interface ListSimulatorsResponse {
simulators: Simulator[]
}
export interface UpdateSimulatorRequest {
id: string
file?: string
type?: string
description?: string
title?: string
}
export interface UpdateSimulatorResponse {
simulator: Simulator
}
export interface DeleteSimulatorResponse {
success: boolean
}
export const simulatorService = {
/**
* Create a new simulator
*/
async createSimulator(data: CreateSimulatorRequest): Promise<Simulator> {
const response = await apiClient.post<CreateSimulatorResponse>('/simulators', data)
return response.simulator
},
/**
* Get a simulator by ID
*/
async getSimulator(id: string): Promise<Simulator> {
const response = await apiClient.get<GetSimulatorResponse>(`/simulators/${id}`)
return response.simulator
},
/**
* List all simulators
*/
async listSimulators(): Promise<Simulator[]> {
const response = await apiClient.get<ListSimulatorsResponse>('/simulators')
return response.simulators || []
},
/**
* Update a simulator
*/
async updateSimulator(data: UpdateSimulatorRequest): Promise<Simulator> {
const { id, ...updateData } = data
const response = await apiClient.put<UpdateSimulatorResponse>(`/simulators/${id}`, updateData)
return response.simulator
},
/**
* Delete a simulator
*/
async deleteSimulator(id: string): Promise<boolean> {
const response = await apiClient.delete<DeleteSimulatorResponse>(`/simulators/${id}`)
return response.success
}
}
+108
View File
@@ -0,0 +1,108 @@
/**
* Task Service
* Handles task-related API calls
*/
import { apiClient } from '../client'
import type { Author } from '../types'
export enum Routine {
NONE = 0,
DAILY = 1,
WEEKLY = 2,
MONTHLY = 3,
YEARLY = 4
}
export interface Task {
id: string
title: string
description: string
deadline: number // Unix timestamp
routine: Routine
tags: string[]
author: Author
}
export interface CreateTaskRequest {
title: string
description?: string
deadline?: number
routine?: Routine
tags?: string[]
author?: Author
}
export interface CreateTaskResponse {
task: Task
}
export interface GetTaskResponse {
task: Task
}
export interface ListTasksResponse {
tasks: Task[]
}
export interface UpdateTaskRequest {
id: string
title?: string
description?: string
deadline?: number
routine?: Routine
tags?: string[]
author?: Author
}
export interface UpdateTaskResponse {
task: Task
}
export interface DeleteTaskResponse {
success: boolean
}
export const taskService = {
/**
* Create a new task
*/
async createTask(data: CreateTaskRequest): Promise<Task> {
const response = await apiClient.post<CreateTaskResponse>('/tasks', data)
return response.task
},
/**
* Get a task by ID
*/
async getTask(id: string): Promise<Task> {
const response = await apiClient.get<GetTaskResponse>(`/tasks/${id}`)
return response.task
},
/**
* List all tasks
*/
async listTasks(): Promise<Task[]> {
const response = await apiClient.get<ListTasksResponse>('/tasks')
return response.tasks || []
},
/**
* Update a task
*/
async updateTask(data: UpdateTaskRequest): Promise<Task> {
const { id, ...updateData } = data
const response = await apiClient.put<UpdateTaskResponse>(`/tasks/${id}`, updateData)
return response.task
},
/**
* Delete a task
*/
async deleteTask(id: string): Promise<boolean> {
const response = await apiClient.delete<DeleteTaskResponse>(`/tasks/${id}`)
return response.success
}
}
+239
View File
@@ -0,0 +1,239 @@
/**
* Todo Service
* Handles todo-related API calls
*/
import { apiClient } from '../client'
export type TodoStatusType = 'pending' | 'in-progress' | 'completed'
export type TodoPriorityType = 'high' | 'medium' | 'low'
export interface Todo {
id: number
title: string
description: string
status: TodoStatusType
priority: TodoPriorityType
startDate?: string // ISO 8601
dueDate?: string // ISO 8601
createdDate: string // ISO 8601
labels: string[]
tags: string[]
isStarred: boolean
isImportant: boolean
isTrashed: boolean
isKanban: boolean
routine: 0 | 1 | 2 | 3 | 4
source: 'todo' | 'calendar' | 'kanban'
}
export interface GetTodosParams {
status?: TodoStatusType
priority?: TodoPriorityType
label?: string
filter?: 'all' | 'starred' | 'important' | 'completed' | 'trashed'
search?: string
}
export interface GetTodosResponse {
todos: Todo[]
total: number
}
export interface CreateTodoRequest {
title: string
description?: string
status?: TodoStatusType
priority?: TodoPriorityType
startDate?: string // ISO 8601
dueDate?: string // ISO 8601
labels?: string[]
tags?: string[]
isStarred?: boolean
isImportant?: boolean
isKanban?: boolean
routine?: 0 | 1 | 2 | 3 | 4
}
export interface CreateTodoResponse {
todo: Todo
}
export interface UpdateTodoRequest {
title?: string
description?: string
status?: TodoStatusType
priority?: TodoPriorityType
startDate?: string
dueDate?: string
labels?: string[]
tags?: string[]
isStarred?: boolean
isImportant?: boolean
isTrashed?: boolean
isKanban?: boolean
routine?: number
}
export interface UpdateTodoResponse {
todo: Todo
}
export interface DeleteTodoResponse {
success: boolean
}
export interface GetLabelsResponse {
labels: string[]
}
// Helper functions for converting between frontend strings and backend enums
const statusToEnum = (status: TodoStatusType): number => {
const statusMap: Record<TodoStatusType, number> = {
'pending': 0,
'in-progress': 1,
'completed': 2
}
return statusMap[status] ?? 0
}
const enumToStatus = (enumValue: number): TodoStatusType => {
const statusMap: Record<number, TodoStatusType> = {
0: 'pending',
1: 'in-progress',
2: 'completed'
}
return statusMap[enumValue] ?? 'pending'
}
const priorityToEnum = (priority: TodoPriorityType): number => {
const priorityMap: Record<TodoPriorityType, number> = {
'high': 0,
'medium': 1,
'low': 2
}
return priorityMap[priority] ?? 1
}
const enumToPriority = (enumValue: number): TodoPriorityType => {
const priorityMap: Record<number, TodoPriorityType> = {
0: 'high',
1: 'medium',
2: 'low'
}
return priorityMap[enumValue] ?? 'medium'
}
// Convert backend Todo response to frontend Todo format
const convertBackendTodoToFrontend = (backendTodo: any): Todo => {
return {
id: backendTodo.id,
title: backendTodo.title,
description: backendTodo.description || '',
status: enumToStatus(backendTodo.status),
priority: enumToPriority(backendTodo.priority),
startDate: backendTodo.start_date || backendTodo.startDate,
dueDate: backendTodo.due_date || backendTodo.dueDate,
createdDate: backendTodo.created_date || backendTodo.createdDate,
labels: backendTodo.labels || [],
tags: backendTodo.tags || [],
isStarred: backendTodo.is_starred ?? backendTodo.isStarred ?? false,
isImportant: backendTodo.is_important ?? backendTodo.isImportant ?? false,
isTrashed: backendTodo.is_trashed ?? backendTodo.isTrashed ?? false,
isKanban: backendTodo.is_kanban ?? backendTodo.isKanban ?? false,
routine: (backendTodo.routine ?? 0) as 0 | 1 | 2 | 3 | 4,
source: backendTodo.source === 0 ? 'todo' : backendTodo.source === 1 ? 'calendar' : 'kanban'
}
}
export const todoService = {
/**
* دریافت لیست Todo با فیلترها
*/
async getTodos(params?: GetTodosParams): Promise<{ todos: Todo[]; total: number }> {
const queryParams = new URLSearchParams()
if (params?.status) queryParams.append('status', params.status)
if (params?.priority) queryParams.append('priority', params.priority)
if (params?.label) queryParams.append('label', params.label)
if (params?.filter) queryParams.append('filter', params.filter)
if (params?.search) queryParams.append('search', params.search)
const queryString = queryParams.toString()
const endpoint = `/api/todos${queryString ? `?${queryString}` : ''}`
const response = await apiClient.get<GetTodosResponse>(endpoint)
const todos = (response.todos || []).map(convertBackendTodoToFrontend)
return {
todos,
total: response.total || 0
}
},
/**
* ایجاد Todo جدید
*/
async createTodo(data: CreateTodoRequest): Promise<Todo> {
// Convert frontend format to backend format
const backendData = {
title: data.title,
description: data.description || '',
status: data.status ? statusToEnum(data.status) : 0, // Default to PENDING
priority: data.priority ? priorityToEnum(data.priority) : 1, // Default to MEDIUM
start_date: data.startDate || '',
due_date: data.dueDate || '',
labels: data.labels || [],
tags: data.tags || [],
is_starred: data.isStarred ?? false,
is_important: data.isImportant ?? false,
is_kanban: data.isKanban ?? false,
routine: data.routine ?? 0 // Default to NONE
}
const response = await apiClient.post<CreateTodoResponse>('/api/todos', backendData)
return convertBackendTodoToFrontend(response.todo)
},
/**
* به‌روزرسانی Todo
*/
async updateTodo(todoId: number, data: UpdateTodoRequest): Promise<Todo> {
// Convert frontend format to backend format
const backendData: any = {}
if (data.title !== undefined) backendData.title = data.title
if (data.description !== undefined) backendData.description = data.description
if (data.status !== undefined) backendData.status = statusToEnum(data.status)
if (data.priority !== undefined) backendData.priority = priorityToEnum(data.priority)
if (data.startDate !== undefined) backendData.start_date = data.startDate
if (data.dueDate !== undefined) backendData.due_date = data.dueDate
if (data.labels !== undefined) backendData.labels = data.labels
if (data.tags !== undefined) backendData.tags = data.tags
if (data.isStarred !== undefined) backendData.is_starred = data.isStarred
if (data.isImportant !== undefined) backendData.is_important = data.isImportant
if (data.isTrashed !== undefined) backendData.is_trashed = data.isTrashed
if (data.isKanban !== undefined) backendData.is_kanban = data.isKanban
if (data.routine !== undefined) backendData.routine = data.routine
const response = await apiClient.put<UpdateTodoResponse>(`/api/todos/${todoId}`, backendData)
return convertBackendTodoToFrontend(response.todo)
},
/**
* حذف Todo
*/
async deleteTodo(todoId: number): Promise<boolean> {
const response = await apiClient.delete<DeleteTodoResponse>(`/api/todos/${todoId}`)
return response.success
},
/**
* دریافت لیست برچسب‌های موجود
*/
async getLabels(): Promise<string[]> {
const response = await apiClient.get<GetLabelsResponse>('/api/todos/labels')
return response.labels || []
}
}
@@ -0,0 +1,96 @@
/**
* User Management Service (Account API)
* Handles account-related API calls. Authenticated user required.
* Routes: GET list, GET by uuid (detail), POST add, PATCH update, DELETE delete, PATCH profile.
*/
import { apiClient } from '../client'
export interface Account {
id: number
username: string
email: string
first_name: string
last_name: string
phone_number: string
}
export interface ApiResponse<T> {
code: number
msg: string
data: T
}
export interface UpdateProfileRequest {
first_name?: string
last_name?: string
email?: string
}
export interface UpdateProfileResponse {
id: number
username: string
email: string
first_name: string
last_name: string
phone_number: string
}
export interface AddAccountRequest {
first_name: string
last_name: string
phones: string[]
}
export interface UpdateAccountRequest {
first_name?: string
last_name?: string
phones?: string[]
}
export const userManagementService = {
/**
* Update current user profile (first_name, last_name, email)
*/
async updateProfile(data: UpdateProfileRequest): Promise<UpdateProfileResponse> {
const response = await apiClient.patch<ApiResponse<UpdateProfileResponse>>('/api/account/profile/', data)
return response.data
},
/**
* Get list of accounts
*/
async getAccounts(): Promise<ApiResponse<unknown>['data']> {
const response = await apiClient.get<ApiResponse<unknown>>('/api/account/')
return response.data
},
/**
* Get one account by uuid
*/
async getAccount(uuid: string): Promise<unknown> {
const response = await apiClient.get<ApiResponse<unknown>>(`/api/account/${uuid}/`)
return response.data
},
/**
* Add a new account
*/
async addAccount(data: AddAccountRequest): Promise<void> {
await apiClient.post<ApiResponse<unknown>>('/api/account/', data)
},
/**
* Update account by uuid
*/
async updateAccount(uuid: string, data: UpdateAccountRequest): Promise<void> {
await apiClient.patch<ApiResponse<unknown>>(`/api/account/${uuid}/`, data)
},
/**
* Delete account by uuid
*/
async deleteAccount(uuid: string): Promise<void> {
await apiClient.delete<ApiResponse<unknown>>(`/api/account/${uuid}/`)
}
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Shared API Types
*/
export interface Author {
name: string
image: string
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Authentication utilities
* Client-side authentication helpers
*/
import type { AuthUser } from './api/services/authService'
/**
* Get authentication token from localStorage
*/
export const getAuthToken = (): string | null => {
if (typeof window === 'undefined') return null
return localStorage.getItem('auth_token')
}
/**
* Get authenticated user from localStorage
*/
export const getAuthUser = (): AuthUser | null => {
if (typeof window === 'undefined') return null
const userStr = localStorage.getItem('auth_user')
if (!userStr) return null
try {
return JSON.parse(userStr)
} catch {
return null
}
}
/**
* Check if user is authenticated
*/
export const isAuthenticated = (): boolean => {
return !!getAuthToken() && !!getAuthUser()
}
/**
* Clear authentication data
*/
export const clearAuth = (): void => {
if (typeof window !== 'undefined') {
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
}
}
+494
View File
@@ -0,0 +1,494 @@
'use client'
// MUI imports
import { styled } from '@mui/material/styles'
import type { Theme } from '@mui/material/styles'
// Styled Components
const AppFullCalendar = styled('div')(({ theme }: { theme: Theme }) => ({
display: 'flex',
position: 'relative',
borderRadius: 'var(--mui-shape-borderRadius)',
'& .fc': {
zIndex: 1,
'.fc-col-header, .fc-daygrid-body, .fc-scrollgrid-sync-table, .fc-timegrid-body, .fc-timegrid-body table': {
width: '100% !important'
},
// Toolbar
'& .fc-toolbar': {
flexWrap: 'wrap',
flexDirection: 'row !important',
'&.fc-header-toolbar': {
gap: theme.spacing(2),
marginBottom: theme.spacing(6)
},
'& .fc-button-group:has(.fc-next-button)': {
marginInlineStart: theme.spacing(2)
},
'& .fc-button': {
padding: theme.spacing(),
'&:active, .&:focus': {
boxShadow: 'none'
}
},
'.fc-prev-button, & .fc-next-button': {
display: 'flex',
backgroundColor: 'transparent',
padding: theme.spacing(1.5),
border: '0px',
'& .fc-icon': {
color: 'var(--mui-palette-text-primary)',
fontSize: '1.25rem'
},
'&:hover, &:active, &:focus': {
boxShadow: 'none !important',
backgroundColor: 'transparent !important'
}
},
'& .fc-toolbar-chunk:first-of-type': {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
rowGap: theme.spacing(2),
[theme.breakpoints.down('md')]: {
'& div:first-of-type': {
display: 'flex',
alignItems: 'center'
}
}
},
'& .fc-button-group': {
'& .fc-button': {
textTransform: 'capitalize',
'&:focus': {
boxShadow: 'none'
}
},
'& .fc-button-primary': {
'&:not(.fc-prev-button):not(.fc-next-button)': {
...theme.typography.button,
textTransform: 'capitalize',
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
padding: theme.spacing(1.75, 4),
color: 'var(--mui-palette-primary-main)',
borderColor: 'transparent',
'&.fc-button-active, &:hover': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
}
}
},
'& .fc-sidebarToggle-button': {
border: '0 !important',
lineHeight: 0.8,
paddingBottom: '0 !important',
backgroundColor: 'transparent !important',
marginInlineEnd: theme.spacing(2),
color: 'var(--mui-palette-text-secondary) !important',
marginLeft: `${theme.spacing(-2)} !important`,
padding: `${theme.spacing(1.25, 2)} !important`,
'&:focus': {
outline: 0,
boxShadow: 'none'
},
'&:not(.fc-prev-button):not(.fc-next-button):hover': {
backgroundColor: 'transparent !important'
},
'& + div': {
marginLeft: 0
}
},
'.fc-dayGridMonth-button, .fc-timeGridWeek-button, .fc-timeGridDay-button, & .fc-listMonth-button': {
padding: theme.spacing(2.2, 6),
'&:last-of-type, &:first-of-type': {
borderRadius: 'var(--mui-shape-borderRadius)'
},
'&:first-of-type': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0
},
'&:last-of-type': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0
}
}
},
'& > * > :not(:first-of-type)': {
marginLeft: 0
},
'& .fc-toolbar-title': {
marginInline: theme.spacing(4),
...theme.typography.h4
},
'.fc-button:empty:not(.fc-sidebarToggle-button), & .fc-toolbar-chunk:empty': {
display: 'none'
}
},
// Calendar head & body common
'& tbody td, & thead th': {
borderColor: 'var(--mui-palette-divider)',
'&.fc-col-header-cell': {
borderLeft: 0,
borderRight: 0
},
'&[role="presentation"]': {
borderInline: 0
}
},
'& colgroup col': {
width: '60px !important'
},
// Event Colors
'& .fc-event': {
'& .fc-event-title-container, .fc-event-main-frame': {
lineHeight: 1
},
'&:not(.fc-list-event)': {
'&.event-bg-primary': {
border: 0,
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
'& .fc-event-title, & .fc-event-time': {
fontSize: theme.typography.caption.fontSize,
fontWeight: 500,
color: 'var(--mui-palette-primary-main)',
padding: 0
}
},
'&.event-bg-success': {
border: 0,
color: 'var(--mui-palette-success-main)',
backgroundColor: 'var(--mui-palette-success-lightOpacity)',
'& .fc-event-title, & .fc-event-time': {
fontSize: theme.typography.caption.fontSize,
fontWeight: 500,
color: 'var(--mui-palette-success-main)',
padding: 0
}
},
'&.event-bg-error': {
border: 0,
color: 'var(--mui-palette-error-main)',
backgroundColor: 'var(--mui-palette-error-lightOpacity)',
'& .fc-event-title, & .fc-event-time': {
fontSize: theme.typography.caption.fontSize,
fontWeight: 500,
color: 'var(--mui-palette-error-main)',
padding: 0
}
},
'&.event-bg-warning': {
border: 0,
color: 'var(--mui-palette-warning-main)',
backgroundColor: 'var(--mui-palette-warning-lightOpacity)',
'& .fc-event-title, & .fc-event-time': {
fontSize: theme.typography.caption.fontSize,
fontWeight: 500,
color: 'var(--mui-palette-warning-main)',
padding: 0
}
},
'&.event-bg-info': {
border: 0,
color: 'var(--mui-palette-info-main)',
backgroundColor: 'var(--mui-palette-info-lightOpacity)',
'& .fc-event-title, & .fc-event-time': {
fontSize: theme.typography.caption.fontSize,
fontWeight: 500,
color: 'var(--mui-palette-info-main)',
padding: 0
}
}
},
'&.event-bg-primary': {
'& .fc-list-event-dot': {
borderColor: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-main)'
},
'&:hover td': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
}
},
'&.event-bg-success': {
'& .fc-list-event-dot': {
borderColor: 'var(--mui-palette-success-main)',
backgroundColor: 'var(--mui-palette-success-main)'
},
'&:hover td': {
backgroundColor: 'var(--mui-palette-success-lightOpacity)'
}
},
'&.event-bg-error': {
'& .fc-list-event-dot': {
borderColor: 'var(--mui-palette-error-main)',
backgroundColor: 'var(--mui-palette-error-main)'
},
'&:hover td': {
backgroundColor: 'var(--mui-palette-error-lightOpacity)'
}
},
'&.event-bg-warning': {
'& .fc-list-event-dot': {
borderColor: 'var(--mui-palette-warning-main)',
backgroundColor: 'var(--mui-palette-warning-main)'
},
'&:hover td': {
backgroundColor: 'var(--mui-palette-warning-lightOpacity)'
}
},
'&.event-bg-info': {
'& .fc-list-event-dot': {
borderColor: 'var(--mui-palette-info-main)',
backgroundColor: 'var(--mui-palette-info-main)'
},
'&:hover td': {
backgroundColor: 'var(--mui-palette-info-lightOpacity)'
}
},
'&.fc-daygrid-event': {
margin: 0,
borderRadius: '500px'
}
},
'& .fc-view-harness': {
minHeight: '650px',
margin: theme.spacing(0, -6)
},
// Calendar Head
'& .fc-col-header': {
'& .fc-col-header-cell-cushion': {
...theme.typography.body1,
fontWeight: 500,
color: 'var(--mui-palette-text-primary)',
padding: theme.spacing(2),
textDecoration: 'none !important'
}
},
// Daygrid
'& .fc-scrollgrid-section-liquid > td': {
borderBottom: 0
},
'& .fc-daygrid-event-harness': {
'& .fc-event': {
padding: theme.spacing(1, 3),
borderRadius: 4
},
'&:not(:last-of-type) .fc-event': {
marginBottom: `${theme.spacing(2.5)} !important`
}
},
'& .fc-daygrid-day-bottom': {
marginTop: theme.spacing(2.5)
},
'& .fc-daygrid-day': {
padding: '8px',
'& .fc-daygrid-day-top': {
flexDirection: 'row'
}
},
'& .fc-scrollgrid': {
borderColor: 'var(--mui-palette-divider)',
borderInline: 0
},
'& .fc-daygrid-day-events': {
marginTop: theme.spacing(2.5),
minHeight: '5rem !important'
},
'& .fc-day-other .fc-daygrid-day-top': {
opacity: 1,
'& .fc-daygrid-day-number': {
color: 'var(--mui-palette-text-disabled) !important'
}
},
// All Views Event
'& .fc-daygrid-day-number, & .fc-timegrid-slot-label-cushion, & .fc-list-event-time': {
textDecoration: 'none !important'
},
'& .fc-daygrid-day-number': {
color: 'var(--mui-palette-text-secondary) !important',
padding: 0
},
'& .fc-timegrid-slot-label-cushion, & .fc-list-event-time': {
color: 'var(--mui-palette-text-primary) !important'
},
'& .fc-day-today:not(.fc-popover)': {
backgroundColor: 'var(--mui-palette-action-hover)'
},
// WeekView
'& .fc-timegrid': {
'& .fc-scrollgrid-section': {
'& .fc-col-header-cell, & .fc-timegrid-axis': {
borderLeft: 0,
borderRight: 0,
background: 'transparent',
borderColor: 'var(--mui-palette-divider)'
},
'& .fc-timegrid-axis': {
borderColor: 'var(--mui-palette-divider)'
},
'& .fc-timegrid-axis-frame': {
justifyContent: 'center',
padding: theme.spacing(2),
alignItems: 'flex-start'
},
'&:has(.fc-timegrid-divider)': {
height: 0
}
},
'& .fc-timegrid-axis': {
'&.fc-scrollgrid-shrink': {
'& .fc-timegrid-axis-cushion': {
...theme.typography.body2,
padding: 0,
textTransform: 'capitalize',
color: 'var(--mui-palette-text-disabled)'
}
}
},
'& .fc-timegrid-slots': {
'& .fc-timegrid-slot': {
height: '3rem',
borderColor: 'var(--mui-palette-divider)',
'&.fc-timegrid-slot-label': {
borderRight: 0,
padding: theme.spacing(2),
verticalAlign: 'top'
},
'&.fc-timegrid-slot-lane': {
borderLeft: 0
},
'& .fc-timegrid-slot-label-frame': {
textAlign: 'center',
'& .fc-timegrid-slot-label-cushion': {
display: 'block',
padding: 0,
...theme.typography.body2,
textTransform: 'uppercase'
}
}
}
},
'& .fc-timegrid-divider': {
display: 'none'
},
'& .fc-timegrid-event': {
'& .fc-event-time': {
...theme.typography.caption,
marginBlockEnd: 2
},
'& .fc-event-title': {
lineHeight: 1.5385
},
boxShadow: 'none'
},
'.fc-timegrid-col-events': {
margin: 0,
'& .fc-event-main': {
padding: theme.spacing(2)
}
}
},
'& .fc-timeGridWeek-view .fc-timegrid-slot-minor': {
borderBlockStart: 0
},
// List View
'& .fc-list': {
border: 'none',
'& th[colspan="3"]': {
position: 'relative'
},
'& .fc-list-day-cushion': {
background: 'transparent',
padding: theme.spacing(2, 4)
},
'.fc-list-event': {
cursor: 'pointer',
'&:hover': {
'& td': {
// backgroundColor: `rgba(${theme.palette.customColors.main}, 0.04)`
}
},
'& td': {
borderColor: 'var(--mui-palette-divider)'
}
},
'& .fc-list-event-graphic': {
padding: theme.spacing(2)
},
'& .fc-list-day': {
backgroundColor: 'var(--mui-palette-action-hover)',
'& .fc-list-day-text, & .fc-list-day-side-text': {
...theme.typography.body1,
fontWeight: 500,
textDecoration: 'none'
},
'& > *': {
background: 'none',
borderColor: 'var(--mui-palette-divider)'
}
},
'& .fc-list-event-title': {
...theme.typography.body1,
color: 'var(--mui-palette-text-secondary) !important',
padding: theme.spacing(2, 4, 2, 2)
},
'& .fc-list-event-time': {
...theme.typography.body1,
color: 'var(--mui-palette-text-secondary) !important',
padding: theme.spacing(2, 4)
},
'.fc-list-table tbody > tr:first-child th': {
borderTop: '1px solid var(--mui-palette-divider)'
},
'.fc-list-table': {
borderBottom: '1px solid var(--mui-palette-divider)'
}
},
// Popover
'& .fc-popover': {
zIndex: 20,
'[data-skin="bordered"] &': {
boxShadow: 'none'
},
boxShadow: 1,
borderColor: 'var(--mui-palette-divider)',
borderRadius: 'var(--mui-shape-borderRadius)',
background: 'var(--mui-palette-background-paper)',
'& .fc-popover-header': {
padding: theme.spacing(2),
borderStartStartRadius: 'var(--mui-shape-borderRadius)',
borderStartEndRadius: 'var(--mui-shape-borderRadius)',
background: 'var(--mui-palette-action-hover)',
'& .fc-popover-title, & .fc-popover-close': {
color: 'var(--mui-palette-text-primary)'
}
}
},
// Media Queries
[theme.breakpoints.up('md')]: {
'& .fc-sidebarToggle-button': {
display: 'none'
},
'& .fc-toolbar-title': {
marginLeft: 0
}
}
}
}))
export default AppFullCalendar
+99
View File
@@ -0,0 +1,99 @@
'use client'
// React Imports
import { useState, useEffect } from 'react'
// MUI imports
import Box from '@mui/material/Box'
import { useTheme } from '@mui/material/styles'
import type { BoxProps } from '@mui/material/Box'
// Third-party Imports
import { Calendar } from 'react-multi-date-picker'
import type { DateObject } from 'react-multi-date-picker'
import persian from 'react-date-object/calendars/persian'
import fa from 'react-date-object/locales/fa'
// Styles - base styles only, we override colors via sx
import 'react-multi-date-picker/styles/colors/teal.css'
interface AppJalaliDatepickerProps {
value?: Date | null
onChange?: (date: Date) => void
boxProps?: BoxProps
}
const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => {
const { value: externalValue, onChange, boxProps } = props
const theme = useTheme()
const [internalValue, setInternalValue] = useState<DateObject | undefined>(() => {
const d = externalValue ?? new Date()
return new DateObject(d, { calendar: persian, locale: fa })
})
useEffect(() => {
if (externalValue != null) {
setInternalValue(new DateObject(externalValue, { calendar: persian, locale: fa }))
}
}, [externalValue])
const handleChange = (d: DateObject | null) => {
if (d) {
setInternalValue(d)
onChange?.(d.toDate())
}
}
const displayValue = internalValue ?? new DateObject({ calendar: persian, locale: fa })
return (
<Box
{...boxProps}
sx={{
...(typeof boxProps?.sx === 'object' ? boxProps.sx : {}),
'& .rmdp-container': {
width: '100%'
},
'& .rmdp-wrapper': {
boxShadow: 'none !important',
border: 'none !important',
backgroundColor: 'var(--mui-palette-background-paper)',
color: 'var(--mui-palette-text-primary)',
fontFamily: theme.typography.fontFamily
},
'& .rmdp-day:not(.rmdp-disabled):not(.rmdp-day-hidden)': {
color: 'var(--mui-palette-text-primary)'
},
'& .rmdp-day.rmdp-selected span:not(.highlight)': {
backgroundColor: 'var(--mui-palette-primary-main) !important',
color: 'var(--mui-palette-common-white) !important'
},
'& .rmdp-day.rmdp-today span': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)'
},
'& .rmdp-week-day': {
color: 'var(--mui-palette-text-primary)'
},
'& .rmdp-header-values': {
color: 'var(--mui-palette-text-primary)'
},
'& .rmdp-arrow': {
borderColor: 'var(--mui-palette-text-secondary)'
}
}}
>
<Calendar
value={displayValue}
onChange={handleChange}
calendar={persian}
locale={fa}
className="teal"
/>
</Box>
)
}
export default AppJalaliDatepicker
+121
View File
@@ -0,0 +1,121 @@
'use client'
// MUI imports
import { styled } from '@mui/material/styles'
import type { Theme } from '@mui/material/styles'
// Third-party Imports
import 'keen-slider/keen-slider.min.css'
// Styled Components
const AppKeenSlider = styled('div')(({ theme }: { theme: Theme }) => ({
'& .keen-slider': {
// Keen Slider handles RTL internally and thus, we need to set the direction to LTR
direction: 'ltr',
'& .keen-slider__slide': {
'& img': {
height: 'auto',
maxWidth: '100%'
}
},
'&.thumbnail .keen-slider__slide:not(.active)': {
opacity: 0.4
},
'&.zoom-out': {
perspective: '1000px',
'& .zoom-out__slide': {
'& .slider-content-wrapper': {
width: '100%',
height: '100%',
position: 'absolute',
'& img': {
width: '100%',
height: '100%',
objectFit: 'cover',
position: 'absolute',
backgroundColor: 'transparent'
}
}
}
},
'& .default-slide': {
height: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'var(--mui-palette-background-default)'
}
},
// Fade
'& .fader': {
position: 'relative',
overflow: 'hidden',
'& .fader__slide': {
width: '100%',
height: '100%',
position: 'absolute',
top: '0',
'& img': {
width: ' 100%',
height: ' 100%',
objectFit: 'cover',
position: 'absolute'
}
}
},
// Navigation Controls
'& .navigation-wrapper': {
position: 'relative',
'& .arrow': {
top: '50%',
width: '3rem',
height: '3rem',
cursor: 'pointer',
position: 'absolute',
transform: 'translateY(-50%)',
color: 'var(--mui-palette-common-white)',
...(theme.direction === 'rtl' ? { transform: 'translateY(-50%) rotate(180deg)' } : {}),
'&.arrow-disabled': {
cursor: 'not-allowed',
pointerEvents: 'none',
color: 'var(--mui-palette-action-disabled)'
},
'&.arrow-left': {
left: 0
},
'&.arrow-right': {
right: 0
}
}
},
// Dots
'& .swiper-dots': {
display: 'flex',
justifyContent: 'center',
marginTop: theme.spacing(4),
'& .MuiBadge-root': {
'&:not(:last-child)': {
marginRight: theme.spacing(4)
},
'& .MuiBadge-dot': {
width: 10,
height: 10,
cursor: 'pointer',
borderRadius: '50%',
backgroundColor: 'var(--mui-palette-action-disabled)'
},
'&.active .MuiBadge-dot': {
backgroundColor: 'var(--mui-palette-primary-main)'
}
}
}
}))
export default AppKeenSlider
+118
View File
@@ -0,0 +1,118 @@
'use client'
// MUI Imports
import Box from '@mui/material/Box'
import { styled } from '@mui/material/styles'
import type { BoxProps } from '@mui/material/Box'
// Third-party Imports
import type { Props } from 'react-apexcharts'
// Component Imports
import ReactApexcharts from '@/libs/ApexCharts'
type ApexChartWrapperProps = Props & {
boxProps?: BoxProps
}
// Styled Components
const ApexChartWrapper = styled(Box)<BoxProps>(({ theme }) => ({
'& .apexcharts-canvas': {
"& line[stroke='transparent']": {
display: 'none'
},
'& .apexcharts-tooltip': {
boxShadow: 'var(--mui-customShadows-xs)',
borderColor: 'var(--mui-palette-divider)',
background: 'var(--mui-palette-background-paper)',
...(theme.direction === 'rtl' && {
'.apexcharts-tooltip-marker': {
marginInlineEnd: 10,
marginInlineStart: 0
},
'.apexcharts-tooltip-text-y-value': {
marginInlineStart: 5,
marginInlineEnd: 0
}
}),
'& .apexcharts-tooltip-title': {
fontWeight: 600,
borderColor: 'var(--mui-palette-divider)',
background: 'var(--mui-palette-background-paper)'
},
'&.apexcharts-theme-light': {
color: 'var(--mui-palette-text-primary)'
},
'&.apexcharts-theme-dark': {
color: 'var(--mui-palette-common-white)'
},
'& .apexcharts-tooltip-series-group:first-of-type': {
paddingBottom: 0
},
'& .bar-chart': {
padding: theme.spacing(2, 2.5)
}
},
'& .apexcharts-xaxistooltip': {
borderColor: 'var(--mui-palette-divider)',
background: 'var(--mui-palette-grey-50)',
...theme.applyStyles('dark', {
background: 'var(--mui-palette-customColors-bodyBg)'
}),
'&:after': {
borderBottomColor: 'var(--mui-palette-grey-50)',
...theme.applyStyles('dark', {
borderBottomColor: 'var(--mui-palette-customColors-bodyBg)'
})
},
'&:before': {
borderBottomColor: 'var(--mui-palette-divider)'
}
},
'& .apexcharts-yaxistooltip': {
borderColor: 'var(--mui-palette-divider)',
background: 'var(--mui-palette-grey-50)',
...theme.applyStyles('dark', {
background: 'var(--mui-palette-customColors-bodyBg)'
}),
'&:after': {
borderLeftColor: 'var(--mui-palette-grey-50)',
...theme.applyStyles('dark', {
borderLeftColor: 'var(--mui-palette-customColors-bodyBg)'
})
},
'&:before': {
borderLeftColor: 'var(--mui-palette-divider)'
}
},
'& .apexcharts-xaxistooltip-text, & .apexcharts-yaxistooltip-text': {
color: 'var(--mui-palette-text-primary)'
},
'& .apexcharts-yaxis .apexcharts-yaxis-texts-g .apexcharts-yaxis-label': {
textAnchor: theme.direction === 'rtl' ? 'start' : undefined
},
'& .apexcharts-text, & .apexcharts-tooltip-text, & .apexcharts-datalabel-label, & .apexcharts-datalabel, & .apexcharts-xaxistooltip-text, & .apexcharts-yaxistooltip-text, & .apexcharts-legend-text':
{
fontFamily: `${theme.typography.fontFamily} !important`
},
'& .apexcharts-pie-label': {
filter: 'none'
},
'& .apexcharts-marker': {
boxShadow: 'none'
}
}
})) as typeof Box
const AppReactApexCharts = (props: ApexChartWrapperProps) => {
// Props
const { boxProps, ...rest } = props
return (
<ApexChartWrapper {...boxProps}>
<ReactApexcharts {...rest} />
</ApexChartWrapper>
)
}
export default AppReactApexCharts
+526
View File
@@ -0,0 +1,526 @@
'use client'
// React Imports
import type { ComponentProps } from 'react'
// MUI imports
import Box from '@mui/material/Box'
import { styled } from '@mui/material/styles'
import type { BoxProps } from '@mui/material/Box'
// Third-party Imports
import ReactDatePickerComponent from 'react-datepicker'
// Styles
import 'react-datepicker/dist/react-datepicker.css'
type Props = ComponentProps<typeof ReactDatePickerComponent> & {
boxProps?: BoxProps
}
// Styled Components
const StyledReactDatePicker = styled(Box)<BoxProps>(({ theme }) => {
return {
'& .react-datepicker-popper': {
zIndex: 20,
paddingTop: `${theme.spacing(0.5)} !important`
},
'& .react-datepicker-wrapper': {
width: '100%'
},
'& .react-datepicker__triangle': {
display: 'none'
},
'& .react-datepicker': {
color: 'var(--mui-palette-text-primary)',
borderRadius: 'var(--mui-shape-borderRadius)',
fontFamily: theme.typography.fontFamily,
backgroundColor: 'var(--mui-palette-background-paper)',
boxShadow: 'var(--mui-customShadows-md)',
border: 'none',
'& .react-datepicker__header': {
padding: 0,
border: 'none',
fontWeight: 'normal',
backgroundColor: 'var(--mui-palette-background-paper)',
'& .react-datepicker__current-month, &.react-datepicker-year-header': {
textAlign: 'left'
},
'&:not(.react-datepicker-year-header)': {
'& + .react-datepicker__month, & + .react-datepicker__year': {
margin: theme.spacing(2),
marginTop: theme.spacing(4.5)
}
},
'&.react-datepicker-year-header': {
'& + .react-datepicker__month, & + .react-datepicker__year': {
margin: theme.spacing(2),
marginTop: theme.spacing(0)
}
}
},
'& > .react-datepicker__navigation': {
top: 13,
borderRadius: '50%',
backgroundColor: 'var(--mui-palette-action-selected)',
'&.react-datepicker__navigation--previous': {
width: 30,
height: 30,
border: 'none',
top: 12,
left: 'auto',
right: '57px',
...(theme.direction === 'ltr'
? {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
...theme.applyStyles('dark', {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
})
}
: {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
...theme.applyStyles('dark', {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
})
}),
'& .react-datepicker__navigation-icon': {
display: 'none'
},
'&:has(+ .react-datepicker__navigation--next--with-time)':
theme.direction === 'ltr' ? { right: 177 } : { left: 177 }
},
'&.react-datepicker__navigation--next': {
width: 30,
height: 30,
border: 'none',
top: 12,
right: 15,
left: 'auto',
...(theme.direction === 'ltr'
? {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
...theme.applyStyles('dark', {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
})
}
: {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
...theme.applyStyles('dark', {
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
})
}),
'& .react-datepicker__navigation-icon': {
display: 'none'
}
},
'&.react-datepicker__navigation--next--with-time': theme.direction === 'ltr' ? { right: 135 } : { left: 135 },
'&:focus, &:active': {
outline: 0
}
},
'& .react-datepicker__current-month, & .react-datepicker-year-header': {
...theme.typography.subtitle1,
lineHeight: 2,
paddingBlockStart: theme.spacing(3),
paddingBlockEnd: theme.spacing(4.5),
paddingInline: theme.spacing(4),
color: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__day-name': {
margin: 0,
width: '2.25rem',
...theme.typography.subtitle2,
color: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__day-names': {
marginBottom: 0
},
'& .react-datepicker__day': {
margin: 0,
width: '2.25rem',
borderRadius: '50%',
lineHeight: '2.25rem',
color: 'var(--mui-palette-text-primary)',
fontSize: theme.typography.body1.fontSize,
'&.react-datepicker__day--selected.react-datepicker__day--in-selecting-range.react-datepicker__day--selecting-range-start, &.react-datepicker__day--selected.react-datepicker__day--range-start.react-datepicker__day--in-range, &.react-datepicker__day--range-start':
{
borderRadius: '18px 0px 0px 18px;',
color: 'var(--mui-palette-common-white) !important',
backgroundColor: 'var(--mui-palette-primary-main) !important'
},
'&.react-datepicker__day--range-end.react-datepicker__day--in-range': {
borderRadius: '0px 18px 18px 0px',
color: 'var(--mui-palette-common-white) !important',
backgroundColor: 'var(--mui-palette-primary-main) !important'
},
'&:focus, &:active': {
outline: 0
},
'&.react-datepicker__day--outside-month, &.react-datepicker__day--disabled:not(.react-datepicker__day--selected)':
{
color: 'var(--mui-palette-text-disabled)',
'&:hover': {
backgroundColor: 'transparent'
}
},
'&.react-datepicker__day--highlighted, &.react-datepicker__day--highlighted:hover': {
color: 'var(--mui-palette-success-main)',
backgroundColor: 'var(--mui-palette-success-lightOpacity)',
'&.react-datepicker__day--selected': {
backgroundColor: 'var(--mui-palette-primary-main) !important'
}
}
},
'&:has(.react-datepicker__day--in-range)': {
'& > .react-datepicker__navigation': {
'&.react-datepicker__navigation--previous': {
...(theme.direction === 'ltr' ? { left: 15 } : { right: 15 })
}
},
'& .react-datepicker__header': {
'& .react-datepicker__current-month': {
textAlign: 'center'
}
}
},
'& .react-datepicker__day--in-range, & .react-datepicker__day--in-selecting-range': {
borderRadius: 0,
color: 'var(--mui-palette-primary-main) !important',
backgroundColor: 'var(--mui-palette-primary-lightOpacity) !important'
},
'& .react-datepicker__day--today': {
fontWeight: 'normal',
'&:not(.react-datepicker__day--selected):not(:empty)': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
},
'&.react-datepicker__day--keyboard-selected': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
}
}
}
},
'& .react-datepicker__month-text--today': {
fontWeight: 'normal',
'&:not(.react-datepicker__month-text--selected)': {
lineHeight: '2.125rem',
color: 'var(--mui-palette-primary-main)',
border: '1px solid var(--mui-palette-primary-main)',
'&:hover': {
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.04)'
}
}
},
'& .react-datepicker__year-text--today': {
fontWeight: 'normal',
'&:not(.react-datepicker__year-text--selected)': {
lineHeight: '2.125rem',
color: 'var(--mui-palette-primary-main)',
border: '1px solid var(--mui-palette-primary-main)',
'&:hover': {
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.04)'
},
'&.react-datepicker__year-text--keyboard-selected': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.06)',
'&:hover': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.06)'
}
}
}
},
'& .react-datepicker__day--keyboard-selected': {
'&:not(.react-datepicker__day--in-range)': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)',
'&:hover': {
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)'
}
}
},
'& .react-datepicker__month-text--keyboard-selected': {
'&:not(.react-datepicker__month--in-range)': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)',
'&:hover': {
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)'
}
}
},
'& .react-datepicker__year-text--keyboard-selected, & .react-datepicker__quarter-text--keyboard-selected': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)'
},
'& .react-datepicker__day--selected, & .react-datepicker__month-text--selected, & .react-datepicker__year-text--selected, & .react-datepicker__quarter-text--selected':
{
color: 'var(--mui-palette-common-white) !important',
backgroundColor: 'var(--mui-palette-primary-main) !important',
boxShadow: 'var(--mui-customShadows-primary-sm)',
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-dark) !important'
}
},
'& .react-datepicker__header__dropdown': {
'& .react-datepicker__month-dropdown-container:not(:last-child)': {
marginRight: theme.spacing(8)
},
'& .react-datepicker__month-dropdown-container, & .react-datepicker__year-dropdown-container': {
marginBottom: theme.spacing(4)
},
'& .react-datepicker__month-read-view--selected-month, & .react-datepicker__year-read-view--selected-year': {
fontSize: '0.875rem',
marginRight: theme.spacing(1),
color: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow, & .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow':
{
borderColor: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__month-read-view--down-arrow, & .react-datepicker__year-read-view--down-arrow': {
top: 4,
borderColor: 'var(--mui-palette-text-secondary)'
},
'& .react-datepicker__month-dropdown, & .react-datepicker__year-dropdown': {
padding: theme.spacing(2),
border: 'none',
borderRadius: 'var(--mui-shape-borderRadius)',
backgroundColor: 'var(--mui-palette-background-paper)',
boxShadow: 'var(--mui-customShadows-lg)',
'[data-skin="bordered"] &': {
boxShadow: 'none',
border: `1px solid var(--mui-palette-divider)`
}
},
'& .react-datepicker__month-option, & .react-datepicker__year-option': {
...theme.typography.body1,
padding: theme.spacing(1.5, 4),
borderRadius: 'var(--mui-shape-borderRadius)',
marginBlockEnd: theme.spacing(0.5),
'&:hover': {
backgroundColor: 'var(--mui-palette-action-hover)'
}
},
'& .react-datepicker__month-option.react-datepicker__month-option--selected_month': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
},
'& .react-datepicker__month-option--selected': {
display: 'none'
}
},
'& .react-datepicker__year-option.react-datepicker__year-option--selected_year': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
},
'& .react-datepicker__year-option--selected': {
display: 'none'
}
},
'& .react-datepicker__year-option': {
// TODO: Remove some of the following styles for arrow in Year dropdown when react-datepicker give arrows in Year dropdown
'& .react-datepicker__navigation--years-upcoming': {
width: 9,
height: 9,
borderStyle: 'solid',
borderWidth: '3px 3px 0 0',
transform: 'rotate(-45deg)',
borderTopColor: 'var(--mui-palette-text-secondary)',
borderRightColor: 'var(--mui-palette-text-secondary)',
margin: `${theme.spacing(2.75)} auto ${theme.spacing(0)}`
},
'&:hover .react-datepicker__navigation--years-upcoming': {
borderTopColor: 'var(--mui-palette-text-primary)',
borderRightColor: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__navigation--years-previous': {
width: 9,
height: 9,
borderStyle: 'solid',
borderWidth: '0 0 3px 3px',
transform: 'rotate(-45deg)',
borderLeftColor: 'var(--mui-palette-text-secondary)',
borderBottomColor: 'var(--mui-palette-text-secondary)',
margin: `${theme.spacing(0)} auto ${theme.spacing(2.75)}`
},
'&:hover .react-datepicker__navigation--years-previous': {
borderLeftColor: 'var(--mui-palette-text-primary)',
borderBottomColor: 'var(--mui-palette-text-primary)'
}
}
},
'& .react-datepicker__week-number': {
margin: 0,
fontWeight: 500,
width: '2.25rem',
lineHeight: '2.25rem',
fontSize: theme.typography.body2.fontSize,
color: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__month-text, & .react-datepicker__year-text, & .react-datepicker__quarter-text': {
margin: 0,
alignItems: 'center',
fontSize: theme.typography.body1.fontSize,
lineHeight: '2rem',
display: 'inline-flex',
justifyContent: 'center',
borderRadius: 'var(--mui-shape-borderRadius)',
'&:focus, &:active': {
outline: 0
}
},
'& .react-datepicker__year-wrapper': {
maxWidth: 205,
justifyContent: 'center'
},
'& .react-datepicker__input-time-container': {
display: 'flex',
alignItems: 'center',
...(theme.direction === 'rtl' ? { flexDirection: 'row-reverse' } : {})
},
'& .react-datepicker__today-button': {
borderTop: 0,
borderRadius: '1rem',
margin: theme.spacing(0, 4, 4),
color: 'var(--mui-palette-common-white)',
backgroundColor: 'var(--mui-palette-primary-main)'
},
// Time Picker
'&:not(.react-datepicker--time-only)': {
'& .react-datepicker__time-container': {
borderLeftColor: 'var(--mui-palette-divider)',
[theme.breakpoints.down('sm')]: {
width: '5.5rem'
},
[theme.breakpoints.up('sm')]: {
width: '7.4375rem'
}
}
},
'&.react-datepicker--time-only': {
width: '7.4375rem',
'& .react-datepicker__time-container': {
width: '7.4375rem'
}
},
'& .react-datepicker__time-container': {
padding: theme.spacing(0.75, 0),
'& .react-datepicker-time__header': {
...theme.typography.subtitle2,
marginBottom: theme.spacing(3.5),
marginTop: theme.spacing(3.5),
color: 'var(--mui-palette-text-primary)'
},
'& .react-datepicker__time': {
background: 'var(--mui-palette-background-paper)',
'& .react-datepicker__time-box .react-datepicker__time-list-item--disabled': {
pointerEvents: 'none',
color: 'var(--mui-palette-text-disabled)',
'&.react-datepicker__time-list-item--selected': {
fontWeight: 'normal',
backgroundColor: 'var(--mui-palette-action-disabledBackground)'
}
}
},
'& .react-datepicker__time-list-item': {
height: 'auto !important',
padding: `${theme.spacing(1.75, 0)} !important`,
marginLeft: theme.spacing(4.25),
marginRight: theme.spacing(2.2),
...theme.typography.body1,
color: 'var(--mui-palette-text-primary)',
borderRadius: 'var(--mui-shape-borderRadius)',
'&:focus, &:active': {
outline: 0
},
'&:hover': {
backgroundColor: 'var(--mui-palette-action-hover) !important'
},
'&.react-datepicker__time-list-item--selected:not(.react-datepicker__time-list-item--disabled)': {
fontWeight: 'normal',
color: 'var(--mui-palette-common-white) !important',
backgroundColor: 'var(--mui-palette-primary-main) !important',
boxShadow: 'var(--mui-customShadows-xs)'
}
},
'& .react-datepicker__time-box': {
width: '100%'
},
'& .react-datepicker__time-list': {
'&::-webkit-scrollbar': {
width: 8
},
/* Track */
'&::-webkit-scrollbar-track': {
background: 'var(--mui-palette-background-paper)'
},
/* Handle */
'&::-webkit-scrollbar-thumb': {
borderRadius: 10,
background: '#aaa'
},
/* Handle on hover */
'&::-webkit-scrollbar-thumb:hover': {
background: '#999'
}
}
},
'& .react-datepicker__day:hover, & .react-datepicker__month-text:hover, & .react-datepicker__quarter-text:hover, & .react-datepicker__year-text:hover':
{
backgroundColor: 'var(--mui-palette-action-hover)'
},
'[data-skin="bordered"] &': {
boxShadow: 'none',
border: `1px solid var(--mui-palette-divider)`
}
},
'& .react-datepicker__close-icon': {
top: 10,
paddingRight: theme.spacing(4),
...(theme.direction === 'rtl' ? { right: 0, left: 'auto' } : {}),
'&:after': {
width: 'unset',
height: 'unset',
fontSize: '1.5rem',
color: 'var(--mui-palette-text-primary)',
backgroundColor: 'transparent !important'
}
}
}
}) as typeof Box
const AppReactDatepicker = (props: Props) => {
// Props
const { boxProps, ...rest } = props
return (
<StyledReactDatePicker {...boxProps}>
<ReactDatePickerComponent popperPlacement='bottom-start' {...rest} />
</StyledReactDatePicker>
)
}
export default AppReactDatepicker
+82
View File
@@ -0,0 +1,82 @@
'use client'
// MUI imports
import Box from '@mui/material/Box'
import { styled } from '@mui/material/styles'
// Type imports
import type { BoxProps } from '@mui/material/Box'
// Styled Components
const AppReactDropzone = styled(Box)<BoxProps>(({ theme }) => ({
'& .dropzone': {
minHeight: 300,
display: 'flex',
flexWrap: 'wrap',
cursor: 'pointer',
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(4),
borderRadius: 'var(--mui-shape-borderRadius)',
border: '2px dashed var(--mui-palette-divider)',
[theme.breakpoints.down('xs')]: {
textAlign: 'center'
},
'&:focus': {
outline: 'none'
},
'& + .MuiList-root': {
padding: 0,
marginTop: theme.spacing(6.25),
'& .MuiListItem-root': {
display: 'flex',
justifyContent: 'space-between',
borderRadius: 'var(--mui-shape-borderRadius)',
padding: theme.spacing(2.5, 2.4, 2.5, 6),
border: '1px solid var(--mui-palette-divider)',
'& .file-details': {
display: 'flex',
alignItems: 'center'
},
'& .file-preview': {
display: 'flex',
marginRight: theme.spacing(3.75),
'& svg': {
fontSize: '2rem'
}
},
'& img': {
width: 38,
height: 38,
padding: theme.spacing(0.75),
borderRadius: 'var(--mui-shape-borderRadius)',
border: '1px solid var(--mui-palette-divider)'
},
'& .file-name': {
fontWeight: 600
},
'& + .MuiListItem-root': {
marginTop: theme.spacing(3.5)
}
},
'& + .buttons': {
display: 'flex',
justifyContent: 'flex-end',
marginTop: theme.spacing(6.25),
'& > :first-of-type': {
marginRight: theme.spacing(3.5)
}
}
},
'& img.single-file-image': {
objectFit: 'cover',
position: 'absolute',
width: 'calc(100% - 1rem)',
height: 'calc(100% - 1rem)',
borderRadius: 'var(--mui-shape-borderRadius)'
}
}
})) as typeof Box
export default AppReactDropzone
+125
View File
@@ -0,0 +1,125 @@
'use client'
// MUI Imports
import Box from '@mui/material/Box'
import useMediaQuery from '@mui/material/useMediaQuery'
import { styled } from '@mui/material/styles'
import type { BoxProps } from '@mui/material/Box'
import type { Theme } from '@mui/material/styles'
// Third-party Imports
import 'react-toastify/dist/ReactToastify.css'
import { ToastContainer } from 'react-toastify'
import type { ToastContainerProps, ToastPosition } from 'react-toastify'
// Type Imports
import type { Direction } from '@core/types'
// Config Imports
import themeConfig from '@configs/themeConfig'
// Hook Imports
import { useSettings } from '@core/hooks/useSettings'
type Props = ToastContainerProps & {
boxProps?: BoxProps
direction?: Direction
}
// Styled Components
const ToastifyWrapper = styled(Box)<BoxProps>(({ theme }) => {
// Hooks
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down(480))
const { settings } = useSettings()
return {
...(isSmallScreen && {
'& .Toastify__toast-container': {
marginBlockStart: theme.spacing(3),
marginInline: theme.spacing(3),
width: 'calc(100dvw - 1.5rem)'
}
}),
'& .Toastify__toast': {
minBlockSize: 46,
borderRadius: 'var(--mui-shape-borderRadius)',
padding: theme.spacing(1.5, 2.5),
backgroundColor: 'var(--mui-palette-background-paper)',
boxShadow: settings.skin === 'bordered' ? 'none' : 'var(--mui-customShadows-md)',
border: settings.skin === 'bordered' && '1px solid var(--mui-palette-divider)',
...(isSmallScreen && {
marginBlockEnd: theme.spacing(4)
}),
'&:not(.custom-toast)': {
'& .Toastify__toast-body': {
color: 'var(--mui-palette-text-primary)'
},
'&.Toastify__toast--success': {
'& .Toastify__toast-icon svg': {
fill: 'var(--mui-palette-success-main)'
}
},
'&.Toastify__toast--error': {
'& .Toastify__toast-icon svg': {
fill: 'var(--mui-palette-error-main)'
}
},
'&.Toastify__toast--warning': {
'& .Toastify__toast-icon svg': {
fill: 'var(--mui-palette-warning-main)'
}
},
'&.Toastify__toast--info': {
'& .Toastify__toast-icon svg': {
fill: 'var(--mui-palette-info-main)'
}
}
},
'[data-skin="bordered"] &': {
boxShadow: 'none',
border: `1px solid var(--mui-palette-divider)`
}
},
'& .Toastify__toast-body': {
margin: 0,
lineHeight: 1.46667,
fontSize: theme.typography.body1.fontSize
},
'& .Toastify__toast-icon': {
marginRight: theme.spacing(3),
height: 20,
width: 20,
'& .Toastify__spinner': {
margin: 3,
height: 14,
width: 14
}
},
'& .Toastify__close-button': {
color: 'var(--mui-palette-text-primary)'
}
}
}) as typeof Box
const AppReactToastify = (props: Props) => {
const { boxProps, direction = 'ltr', ...rest } = props
const positionMap: Partial<Record<ToastPosition, ToastPosition>> = {
'top-right': 'top-left',
'top-left': 'top-right',
'bottom-left': 'bottom-right',
'bottom-right': 'bottom-left',
'top-center': 'top-center',
'bottom-center': 'bottom-center'
}
const position = direction === 'rtl' ? positionMap[themeConfig.toastPosition] : themeConfig.toastPosition
return (
<ToastifyWrapper {...boxProps}>
<ToastContainer rtl={direction === 'rtl'} position={position} {...rest} />
</ToastifyWrapper>
)
}
export default AppReactToastify
+55
View File
@@ -0,0 +1,55 @@
'use client'
// MUI imports
import { styled } from '@mui/material/styles'
// Styled Components
const AppRecharts = styled('div')(({ theme }) => ({
'& .recharts-cartesian-grid-vertical, & .recharts-cartesian-grid-horizontal, & .recharts-polar-grid-angle, & .recharts-polar-radius-axis, & .recharts-cartesian-axis':
{
'& line': {
stroke: 'var(--mui-palette-divider)'
}
},
'& .recharts-polar-grid-concentric-polygon': {
stroke: 'var(--mui-palette-divider)'
},
'& .recharts-tooltip-wrapper': {
outline: 'none'
},
'& .recharts-default-tooltip': {
border: 'none !important',
boxShadow: 'var(--mui-customShadows-xs)',
borderRadius: 'var(--mui-shape-borderRadius)',
backgroundColor: 'var(--mui-palette-background-paper) !important'
},
'& .recharts-custom-tooltip': {
padding: theme.spacing(2.5),
boxShadow: 'var(--mui-customShadows-xs)',
borderRadius: 'var(--mui-shape-borderRadius)',
backgroundColor: 'var(--mui-palette-background-paper)'
},
'& .recharts-tooltip-cursor': {
fill: 'var(--mui-palette-action-hover)'
},
'& .recharts-yAxis .recharts-cartesian-axis-ticks .recharts-cartesian-axis-tick .recharts-cartesian-axis-tick-value':
{
textAnchor: theme.direction === 'rtl' ? 'end' : undefined
},
'& .recharts-active-dot .recharts-dot': {
fill: 'var(--mui-palette-secondary-main)'
},
'& .recharts-tooltip-item': {
fontSize: '0.875rem',
color: 'var(--mui-palette-text-primary) !important'
},
'& .recharts-text': {
fontSize: '0.8125rem',
fill: 'var(--mui-palette-text-disabled)'
},
'& .recharts-pie .recharts-sector, & .recharts-layer': {
outline: 'none !important'
}
}))
export default AppRecharts
+39
View File
@@ -0,0 +1,39 @@
.slot {
position: relative;
inline-size: 100%;
block-size: 3.5rem;
font-size: 1.25rem;
display: flex;
justify-content: center;
align-items: center;
transition: all 100ms;
border-width: 1px;
border-radius: var(--border-radius);
}
.slotActive {
outline: 1px solid var(--mui-palette-primary-main);
border-color: var(--mui-palette-primary-main);
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
.fakeCaret {
position: absolute;
pointer-events: none;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
animation: caret-blink 1.2s ease-out infinite;
}
+72
View File
@@ -0,0 +1,72 @@
/* Basic editor styles */
.ProseMirror {
outline: none;
min-block-size: 100px;
overflow-y: auto;
padding: 1.5rem;
inline-size: 100%;
> * + * {
margin-block-start: 0.75em;
}
p.is-editor-empty:first-child::before {
block-size: 0;
color: var(--mui-palette-text-disabled);
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
ul,
ol {
padding-block: 0;
padding-inline: 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0d0d0d;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding-block: 0.75rem;
padding-inline: 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-inline-size: 100%;
block-size: auto;
}
blockquote {
padding-inline-start: 1rem;
border-inline-start: 2px solid var(--mui-palette-divider);
}
hr {
border: none;
border-block-start: 2px solid var(--mui-palette-divider);
margin-block: 2rem;
margin-inline: 0;
}
}