First commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false })
|
||||
|
||||
export default Chart
|
||||
@@ -0,0 +1,5 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const ReactPlayer = dynamic(() => import('react-player'), { ssr: false })
|
||||
|
||||
export default ReactPlayer
|
||||
@@ -0,0 +1,4 @@
|
||||
'use client'
|
||||
|
||||
// Third-party imports
|
||||
export * from 'recharts'
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 || []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 || []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}/`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Shared API Types
|
||||
*/
|
||||
|
||||
export interface Author {
|
||||
name: string
|
||||
image: string
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user