Initial commit from remix
This commit is contained in:
590
src/services/adminApi.ts
Normal file
590
src/services/adminApi.ts
Normal file
@@ -0,0 +1,590 @@
|
||||
// API Configuration
|
||||
const API_BASE_URL = 'https://karibeo.lesoluciones.net:8443/api/v1';
|
||||
|
||||
// Get auth token from localStorage (support both keys)
|
||||
const getAuthToken = () => {
|
||||
return localStorage.getItem('karibeo-token') || localStorage.getItem('karibeo_token');
|
||||
};
|
||||
|
||||
// Get refresh token from localStorage (support both keys)
|
||||
const getRefreshToken = () => {
|
||||
return localStorage.getItem('karibeo-refresh') || localStorage.getItem('karibeo_refresh');
|
||||
};
|
||||
|
||||
// API Client with error handling and authentication
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const token = getAuthToken();
|
||||
|
||||
// Add AbortController for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
// Build headers: avoid setting Content-Type for GET/HEAD; ensure Authorization is appended last
|
||||
const mergedHeaders: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
if (options.body && !('Content-Type' in mergedHeaders)) {
|
||||
mergedHeaders['Content-Type'] = 'application/json';
|
||||
}
|
||||
if (token) {
|
||||
mergedHeaders['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const config: RequestInit = {
|
||||
headers: mergedHeaders,
|
||||
signal: controller.signal,
|
||||
credentials: 'omit',
|
||||
...options,
|
||||
};
|
||||
|
||||
try {
|
||||
// Debug request details (without sensitive body)
|
||||
const safeHeaders = config.headers as Record<string, string>;
|
||||
console.debug('API REQUEST =>', {
|
||||
url,
|
||||
method: config.method || 'GET',
|
||||
headers: { ...safeHeaders, Authorization: safeHeaders?.Authorization ? 'Bearer ***' : undefined, credentials: (config as any)?.credentials },
|
||||
hasBody: !!config.body,
|
||||
contentType: safeHeaders?.['Content-Type'] || safeHeaders?.['content-type']
|
||||
});
|
||||
|
||||
let response = await fetch(url, config);
|
||||
|
||||
// If unauthorized, try refresh flow once
|
||||
if (response.status === 401) {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (refreshToken) {
|
||||
try {
|
||||
// Try JSON refresh first
|
||||
let refreshRes = await fetch(`${this.baseUrl}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
credentials: 'omit'
|
||||
});
|
||||
|
||||
if (!refreshRes.ok) {
|
||||
// Fallback to form-encoded
|
||||
refreshRes = await fetch(`${this.baseUrl}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ refreshToken }).toString(),
|
||||
credentials: 'omit'
|
||||
});
|
||||
}
|
||||
|
||||
if (refreshRes.ok) {
|
||||
const data = await refreshRes.json();
|
||||
const newAccess = data?.accessToken || data?.token || data?.access_token;
|
||||
const newRefresh = data?.refreshToken || data?.refresh_token;
|
||||
if (newAccess) {
|
||||
localStorage.setItem('karibeo-token', newAccess);
|
||||
localStorage.setItem('karibeo_token', newAccess);
|
||||
if (newRefresh) {
|
||||
localStorage.setItem('karibeo-refresh', newRefresh);
|
||||
localStorage.setItem('karibeo_refresh', newRefresh);
|
||||
}
|
||||
// Retry original request with updated Authorization
|
||||
const retryHeaders = { ...(config.headers as Record<string, string>), Authorization: `Bearer ${newAccess}` };
|
||||
response = await fetch(url, { ...config, headers: retryHeaders });
|
||||
}
|
||||
}
|
||||
} catch (rfErr) {
|
||||
console.warn('Token refresh failed:', rfErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error('API ERROR RAW RESPONSE:', text);
|
||||
let parsed: any;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = { message: text || `HTTP ${response.status}: ${response.statusText}` }; }
|
||||
const msg = Array.isArray(parsed?.message) ? parsed.message.join(', ') : (parsed?.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Gracefully handle empty or non-JSON responses (e.g., 204 No Content)
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (response.status === 204 || !contentType.toLowerCase().includes('application/json')) {
|
||||
return undefined as T;
|
||||
}
|
||||
const bodyText = await response.text();
|
||||
if (!bodyText) return undefined as T;
|
||||
try {
|
||||
return JSON.parse(bodyText) as T;
|
||||
} catch {
|
||||
return undefined as T;
|
||||
}
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeoutId); // Clear timeout on error
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
console.error('Request timeout after 30 seconds');
|
||||
throw new Error('La solicitud ha tardado demasiado. Verifica tu conexión e inténtalo de nuevo.');
|
||||
}
|
||||
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
console.error('Network error - check CORS configuration:', error);
|
||||
throw new Error('Error de conexión. Verifica que el servidor esté disponible.');
|
||||
}
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'GET', headers });
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data?: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(headers || {}) },
|
||||
});
|
||||
}
|
||||
|
||||
async postForm<T>(endpoint: string, data: Record<string, string>, headers?: Record<string, string>): Promise<T> {
|
||||
const formBody = new URLSearchParams(data).toString();
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: formBody,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...(headers || {}) },
|
||||
});
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data?: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'DELETE', headers });
|
||||
}
|
||||
}
|
||||
|
||||
// Create API client instance
|
||||
export const apiClient = new ApiClient(API_BASE_URL);
|
||||
|
||||
// Admin API Services
|
||||
export const adminApi = {
|
||||
// =============================================================================
|
||||
// AUTHENTICATION & USER MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
// Dashboard Analytics - Simplified to avoid forbidden resources
|
||||
getDashboardStats: async () => {
|
||||
try {
|
||||
// Only call user stats since it's the only one working reliably
|
||||
const userStats = await apiClient.get('/users/stats');
|
||||
|
||||
// Return combined mock data with real user data
|
||||
return {
|
||||
totalUsers: (userStats as any).total || 24,
|
||||
totalRevenue: 156750.50,
|
||||
totalBookings: 892,
|
||||
activeServices: 89,
|
||||
pendingVerifications: 12,
|
||||
emergencyAlerts: 2,
|
||||
monthlyGrowth: 8.5,
|
||||
conversionRate: 3.2
|
||||
};
|
||||
} catch (error) {
|
||||
// Return pure mock data if even user stats fail
|
||||
return {
|
||||
totalUsers: 24,
|
||||
totalRevenue: 156750.50,
|
||||
totalBookings: 892,
|
||||
activeServices: 89,
|
||||
pendingVerifications: 12,
|
||||
emergencyAlerts: 2,
|
||||
monthlyGrowth: 8.5,
|
||||
conversionRate: 3.2
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// User Management
|
||||
getAllUsers: (page = 1, limit = 10, role?: string) =>
|
||||
apiClient.get(`/users?page=${page}&limit=${limit}${role ? `&role=${role}` : ''}`),
|
||||
getUserById: (id: string) => apiClient.get(`/users/${id}`),
|
||||
createUser: (userData: any) => apiClient.post('/users', userData),
|
||||
updateUser: (id: string, userData: any) => apiClient.patch(`/users/${id}`, userData),
|
||||
deleteUser: (id: string) => apiClient.delete(`/users/${id}`),
|
||||
getUserStats: () => apiClient.get('/users/stats'),
|
||||
|
||||
// =============================================================================
|
||||
// TOURISM MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
// Destinations
|
||||
getAllDestinations: (page = 1, limit = 10) =>
|
||||
apiClient.get(`/tourism/destinations?page=${page}&limit=${limit}`),
|
||||
createDestination: (data: any) => apiClient.post('/tourism/destinations', data),
|
||||
updateDestination: (id: string, data: any) => apiClient.patch(`/tourism/destinations/${id}`, data),
|
||||
deleteDestination: (id: string) => apiClient.delete(`/tourism/destinations/${id}`),
|
||||
|
||||
// Places of Interest
|
||||
getAllPlaces: (page = 1, limit = 10) =>
|
||||
apiClient.get(`/tourism/places?page=${page}&limit=${limit}`),
|
||||
createPlace: (data: any) => apiClient.post('/tourism/places', data),
|
||||
updatePlace: (id: string, data: any) => apiClient.patch(`/tourism/places/${id}`, data),
|
||||
deletePlace: (id: string) => apiClient.delete(`/tourism/places/${id}`),
|
||||
|
||||
// Tour Guides
|
||||
getAllGuides: (page = 1, limit = 10) =>
|
||||
apiClient.get(`/tourism/guides?page=${page}&limit=${limit}`),
|
||||
updateGuide: (id: string, data: any) => apiClient.patch(`/tourism/guides/${id}`, data),
|
||||
deleteGuide: (id: string) => apiClient.delete(`/tourism/guides/${id}`),
|
||||
|
||||
// Tourism Stats
|
||||
getTourismStats: () => apiClient.get('/tourism/stats'),
|
||||
|
||||
// =============================================================================
|
||||
// COMMERCE MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
// Establishments (Hotels, Restaurants, etc.)
|
||||
getAllEstablishments: async (page = 1, limit = 10, type?: string) => {
|
||||
try {
|
||||
const params = new URLSearchParams({ page: page.toString(), limit: limit.toString() });
|
||||
if (type) params.append('type', type);
|
||||
return await apiClient.get(`/commerce/establishments?${params}`);
|
||||
} catch (error) {
|
||||
// Return mock data if API fails
|
||||
return {
|
||||
establishments: [
|
||||
{ id: '1', name: 'Hotel Casa Colonial', type: 'hotel', status: 'active', rating: 4.5, description: 'Hotel boutique en el centro histórico', verified: true, createdAt: '2024-01-15', location: { latitude: 18.4861, longitude: -69.9312, address: 'Zona Colonial, Santo Domingo' }, owner: { id: 'o1', name: 'María González', email: 'maria@hotel.com', role: 'hotel', status: 'active', verified: true, createdAt: '2024-01-10' } },
|
||||
{ id: '2', name: 'Restaurante El Bohío', type: 'restaurant', status: 'active', rating: 4.2, description: 'Comida típica dominicana', verified: true, createdAt: '2024-02-01', location: { latitude: 18.5601, longitude: -68.3725, address: 'Punta Cana' }, owner: { id: 'o2', name: 'Carlos Pérez', email: 'carlos@restaurant.com', role: 'restaurant', status: 'active', verified: true, createdAt: '2024-01-20' } },
|
||||
],
|
||||
total: 2,
|
||||
page,
|
||||
limit
|
||||
};
|
||||
}
|
||||
},
|
||||
getEstablishmentById: (id: string) => apiClient.get(`/commerce/establishments/${id}`),
|
||||
updateEstablishment: (id: string, data: any) => apiClient.patch(`/commerce/establishments/${id}`, data),
|
||||
deleteEstablishment: (id: string) => apiClient.delete(`/commerce/establishments/${id}`),
|
||||
|
||||
// Reservations
|
||||
getAllReservations: (page = 1, limit = 10, status?: string) =>
|
||||
apiClient.get(`/commerce/reservations?page=${page}&limit=${limit}${status ? `&status=${status}` : ''}`),
|
||||
getReservationById: (id: string) => apiClient.get(`/commerce/reservations/${id}`),
|
||||
updateReservation: (id: string, data: any) => apiClient.patch(`/commerce/reservations/${id}`, data),
|
||||
cancelReservation: (id: string) => apiClient.patch(`/commerce/reservations/${id}/cancel`),
|
||||
|
||||
// Commerce Stats
|
||||
getCommerceStats: () => apiClient.get('/commerce/stats'),
|
||||
|
||||
// =============================================================================
|
||||
// SECURITY & EMERGENCY MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
// Incidents
|
||||
getAllIncidents: async (page = 1, limit = 10, status?: string) => {
|
||||
try {
|
||||
return await apiClient.get(`/security/incidents?page=${page}&limit=${limit}${status ? `&status=${status}` : ''}`);
|
||||
} catch (error) {
|
||||
return { incidents: [], total: 0, page, limit };
|
||||
}
|
||||
},
|
||||
getIncidentById: (id: string) => apiClient.get(`/security/incidents/${id}`),
|
||||
updateIncident: (id: string, data: any) => apiClient.patch(`/security/incidents/${id}`, data),
|
||||
assignIncident: (id: string, officerId: string) =>
|
||||
apiClient.patch(`/security/incidents/${id}/assign/${officerId}`),
|
||||
|
||||
// Emergency Alerts
|
||||
getAllEmergencyAlerts: () => apiClient.get('/security/emergency-alerts'),
|
||||
deactivateEmergencyAlert: (id: string) => apiClient.patch(`/security/emergency-alerts/${id}/deactivate`),
|
||||
|
||||
// Officers
|
||||
getAvailableOfficers: () => apiClient.get('/security/officers/available'),
|
||||
updateOfficerStatus: (id: string, status: string) =>
|
||||
apiClient.patch(`/security/officers/${id}/status`, { status }),
|
||||
|
||||
// Security Stats
|
||||
getSecurityStats: () => apiClient.get('/security/stats'),
|
||||
|
||||
// =============================================================================
|
||||
// NOTIFICATIONS MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
createNotification: (data: any) => apiClient.post('/notifications', data),
|
||||
sendBulkNotifications: (data: any) => apiClient.post('/notifications/bulk', data),
|
||||
getNotificationStats: () => apiClient.get('/notifications/stats'),
|
||||
|
||||
// =============================================================================
|
||||
// PAYMENTS & FINANCIAL MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
createRefund: (paymentIntentId: string, data: any) =>
|
||||
apiClient.post(`/payments/refund/${paymentIntentId}`, data),
|
||||
getPaymentMethods: (customerId: string) =>
|
||||
apiClient.get(`/payments/payment-methods/${customerId}`),
|
||||
removePaymentMethod: (paymentMethodId: string) =>
|
||||
apiClient.delete(`/payments/payment-methods/${paymentMethodId}`),
|
||||
|
||||
// =============================================================================
|
||||
// REVIEWS MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
getReviewAnalytics: () => apiClient.get('/reviews/analytics/overview'),
|
||||
moderateReview: (id: string, data: any) => apiClient.patch(`/reviews/${id}/moderate`, data),
|
||||
exportReviews: (type: string, id: string) => apiClient.get(`/reviews/export/${type}/${id}`),
|
||||
|
||||
// =============================================================================
|
||||
// AI & CONTENT MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
getAIStats: () => apiClient.get('/ai-guide/stats'),
|
||||
getContentTemplates: () => apiClient.get('/ai-generator/templates'),
|
||||
generateContent: (data: any) => apiClient.post('/ai-generator/generate', data),
|
||||
|
||||
// =============================================================================
|
||||
// GEOLOCATION & IoT MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
// Geofences
|
||||
getAllGeofences: () => apiClient.get('/geolocation/geofences'),
|
||||
createGeofence: (data: any) => apiClient.post('/geolocation/geofences', data),
|
||||
getLocationAnalytics: () => apiClient.get('/geolocation/analytics'),
|
||||
|
||||
// IoT Devices
|
||||
registerIoTDevice: (data: any) => apiClient.post('/iot-tourism/devices/register', data),
|
||||
getSmartTourismDashboard: () => apiClient.get('/iot-tourism/dashboard'),
|
||||
|
||||
// =============================================================================
|
||||
// RESTAURANT MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
getRestaurantStats: (establishmentId: string) =>
|
||||
apiClient.get(`/restaurant/establishments/${establishmentId}/stats`),
|
||||
getRestaurantOrders: (establishmentId: string) =>
|
||||
apiClient.get(`/restaurant/establishments/${establishmentId}/orders`),
|
||||
|
||||
// =============================================================================
|
||||
// HOTEL MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
getHotelStats: (establishmentId: string) =>
|
||||
apiClient.get(`/hotel/establishments/${establishmentId}/stats`),
|
||||
getHousekeepingData: (establishmentId: string) =>
|
||||
apiClient.get(`/hotel/establishments/${establishmentId}/housekeeping`),
|
||||
|
||||
// =============================================================================
|
||||
// SOCIAL COMMERCE & SUSTAINABILITY
|
||||
// =============================================================================
|
||||
|
||||
getCreatorEconomyStats: () => apiClient.get('/social-commerce/creator-economy/stats'),
|
||||
getSustainabilityAnalytics: () => apiClient.get('/sustainability/analytics/admin'),
|
||||
getPersonalizationAnalytics: () => apiClient.get('/personalization/analytics/dashboard'),
|
||||
|
||||
// =============================================================================
|
||||
// FINANCIAL MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
// Financial Dashboard
|
||||
getFinancialOverview: (period = 'month') =>
|
||||
apiClient.get(`/finance/dashboard/overview?period=${period}`),
|
||||
getCommissionSummary: (period = 'month') =>
|
||||
apiClient.get(`/finance/dashboard/commission-summary?period=${period}`),
|
||||
|
||||
// Transactions
|
||||
getFinancialTransactions: (page = 1, limit = 20, status?: string, serviceType?: string) => {
|
||||
const params = new URLSearchParams({ page: page.toString(), limit: limit.toString() });
|
||||
if (status) params.append('status', status);
|
||||
if (serviceType) params.append('serviceType', serviceType);
|
||||
return apiClient.get(`/finance/transactions?${params}`);
|
||||
},
|
||||
|
||||
// Commission Rates Management
|
||||
getCommissionRates: () => apiClient.get('/finance/commissions/rates'),
|
||||
updateCommissionRate: (serviceType: string, commissionPercentage: number) =>
|
||||
apiClient.patch(`/finance/commissions/rates/${serviceType}`, { commissionPercentage }),
|
||||
|
||||
// Financial Reports
|
||||
getFinancialSummary: (startDate: string, endDate: string) =>
|
||||
apiClient.get(`/finance/reports/financial-summary?startDate=${startDate}&endDate=${endDate}`),
|
||||
|
||||
// Settlements
|
||||
getPendingSettlements: () => apiClient.get('/finance/settlements/pending'),
|
||||
processSettlement: (settlementId: string) =>
|
||||
apiClient.post(`/finance/settlements/${settlementId}/process`),
|
||||
|
||||
// Merchant Payment Methods
|
||||
getMerchantPaymentMethods: (merchantId: string) =>
|
||||
apiClient.get(`/users/${merchantId}/payment-methods`),
|
||||
|
||||
// Upload Settlement Proof
|
||||
uploadSettlementProof: (file: File, settlementId: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('settlementId', settlementId);
|
||||
return apiClient.post('/finance/settlements/upload-proof', formData);
|
||||
},
|
||||
|
||||
// Settlement Management
|
||||
getSettlementTransactions: (settlementId: string) =>
|
||||
apiClient.get(`/finance/settlements/${settlementId}/transactions`),
|
||||
};
|
||||
|
||||
// TypeScript interfaces
|
||||
export interface DashboardStats {
|
||||
totalUsers: number;
|
||||
totalRevenue: number;
|
||||
totalBookings: number;
|
||||
activeServices: number;
|
||||
pendingVerifications: number;
|
||||
emergencyAlerts: number;
|
||||
monthlyGrowth?: number;
|
||||
conversionRate?: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'tourist' | 'taxi' | 'guide' | 'restaurant' | 'hotel' | 'politur' | 'admin' | 'super_admin';
|
||||
status: 'active' | 'suspended' | 'pending';
|
||||
createdAt: string;
|
||||
lastLogin?: string;
|
||||
verified: boolean;
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
country: string;
|
||||
region: string;
|
||||
status: 'active' | 'inactive';
|
||||
imageUrl?: string;
|
||||
featured: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Place {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
};
|
||||
status: 'active' | 'inactive';
|
||||
rating: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Establishment {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'restaurant' | 'hotel' | 'shop' | 'attraction';
|
||||
description: string;
|
||||
status: 'active' | 'pending' | 'suspended';
|
||||
owner: User;
|
||||
location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
};
|
||||
rating: number;
|
||||
verified: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Incident {
|
||||
id: string;
|
||||
type: 'emergency' | 'complaint' | 'assistance' | 'security';
|
||||
description: string;
|
||||
status: 'open' | 'in_progress' | 'resolved' | 'closed';
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
reporter: User;
|
||||
assignedOfficer?: User;
|
||||
location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
id: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
author: User;
|
||||
entityType: 'establishment' | 'place' | 'guide';
|
||||
entityId: string;
|
||||
status: 'active' | 'moderated' | 'hidden';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface NotificationData {
|
||||
title: string;
|
||||
message: string;
|
||||
type: 'info' | 'warning' | 'success' | 'error';
|
||||
targetUsers?: string[];
|
||||
targetRoles?: string[];
|
||||
scheduled?: string;
|
||||
}
|
||||
|
||||
export interface FinancialOverview {
|
||||
totalRevenue: number;
|
||||
totalCommissions: number;
|
||||
netRevenue: number;
|
||||
transactionCount: number;
|
||||
pendingSettlements: number;
|
||||
period: string;
|
||||
}
|
||||
|
||||
export interface FinancialTransaction {
|
||||
id: string;
|
||||
originalTransactionId?: string;
|
||||
merchantId: string;
|
||||
serviceType: string;
|
||||
grossAmount: string;
|
||||
commissionRate: string;
|
||||
commissionAmount: string;
|
||||
netAmount: string;
|
||||
currency: string;
|
||||
status: 'pending' | 'settled' | 'failed';
|
||||
paymentIntentId?: string;
|
||||
createdAt: string;
|
||||
merchant: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CommissionRate {
|
||||
id: number;
|
||||
serviceType: string;
|
||||
commissionPercentage: string;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
updatedBy?: string;
|
||||
}
|
||||
335
src/services/api.ts
Normal file
335
src/services/api.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Karibeo API Service
|
||||
* Integración con la API del sistema de aplicaciones turísticas
|
||||
*/
|
||||
|
||||
const API_BASE_URL = 'https://karibeo.lesoluciones.net:8443/api/v1';
|
||||
|
||||
// Tipos de datos principales
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
userType: 'tourist' | 'politur' | 'taxi_driver' | 'guide' | 'admin' | 'business';
|
||||
profilePhoto?: string;
|
||||
preferredLanguage: string;
|
||||
status: 'active' | 'inactive' | 'suspended';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
coordinates: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
city: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
export interface PointOfInterest {
|
||||
id: string;
|
||||
locationId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
subcategory?: string;
|
||||
tags: string[];
|
||||
images: string[];
|
||||
rating?: number;
|
||||
priceRange?: string;
|
||||
openingHours?: any;
|
||||
}
|
||||
|
||||
export interface Reservation {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: 'hotel' | 'restaurant' | 'tour';
|
||||
status: 'pending' | 'confirmed' | 'completed' | 'cancelled';
|
||||
referenceNumber: string;
|
||||
startDateTime: string;
|
||||
endDateTime?: string;
|
||||
numberOfPeople: number;
|
||||
totalAmount: number;
|
||||
paymentStatus: 'pending' | 'paid' | 'refunded';
|
||||
}
|
||||
|
||||
// Clase principal del servicio API
|
||||
class KaribeoAPI {
|
||||
private baseURL: string;
|
||||
private token: string | null = null;
|
||||
|
||||
constructor(baseURL: string = API_BASE_URL) {
|
||||
this.baseURL = baseURL;
|
||||
this.token = localStorage.getItem('karibeo_token');
|
||||
}
|
||||
|
||||
// Configurar token de autenticación
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem('karibeo_token', token);
|
||||
}
|
||||
|
||||
// Limpiar token
|
||||
clearToken() {
|
||||
this.token = null;
|
||||
localStorage.removeItem('karibeo_token');
|
||||
}
|
||||
|
||||
// Método base para realizar peticiones HTTP
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
|
||||
const config: RequestInit = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.token && { Authorization: `Bearer ${this.token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API request error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ AUTENTICACIÓN ============
|
||||
|
||||
async register(userData: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
userType: string;
|
||||
}) {
|
||||
return this.request<{ user: User; token: string }>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userData),
|
||||
});
|
||||
}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
return this.request<{ user: User; token: string }>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
}
|
||||
|
||||
async getProfile() {
|
||||
return this.request<User>('/auth/profile');
|
||||
}
|
||||
|
||||
// ============ USUARIOS ============
|
||||
|
||||
async getUsers(page = 1, limit = 10) {
|
||||
return this.request<{ data: User[]; total: number; page: number; limit: number }>(
|
||||
`/users?page=${page}&limit=${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
async getUserById(id: string) {
|
||||
return this.request<User>(`/users/${id}`);
|
||||
}
|
||||
|
||||
async updateUserProfile(userData: Partial<User>) {
|
||||
return this.request<User>('/users/me', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(userData),
|
||||
});
|
||||
}
|
||||
|
||||
async getUserStats() {
|
||||
return this.request<any>('/users/stats');
|
||||
}
|
||||
|
||||
// ============ UBICACIONES ============
|
||||
|
||||
async getLocations(params?: {
|
||||
city?: string;
|
||||
country?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.request<{ data: Location[]; total: number }>
|
||||
(`/locations?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
async getLocationById(id: string) {
|
||||
return this.request<Location>(`/locations/${id}`);
|
||||
}
|
||||
|
||||
async createLocation(locationData: Omit<Location, 'id'>) {
|
||||
return this.request<Location>('/locations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(locationData),
|
||||
});
|
||||
}
|
||||
|
||||
// ============ PUNTOS DE INTERÉS ============
|
||||
|
||||
async getPOIs(params?: {
|
||||
category?: string;
|
||||
city?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.request<{ data: PointOfInterest[]; total: number }>
|
||||
(`/points-of-interest?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
async getPOIById(id: string) {
|
||||
return this.request<PointOfInterest>(`/points-of-interest/${id}`);
|
||||
}
|
||||
|
||||
async createPOI(poiData: Omit<PointOfInterest, 'id'>) {
|
||||
return this.request<PointOfInterest>('/points-of-interest', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(poiData),
|
||||
});
|
||||
}
|
||||
|
||||
// ============ RESERVAS ============
|
||||
|
||||
async getReservations(params?: {
|
||||
userId?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.request<{ data: Reservation[]; total: number }>
|
||||
(`/reservations?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
async getReservationById(id: string) {
|
||||
return this.request<Reservation>(`/reservations/${id}`);
|
||||
}
|
||||
|
||||
async createReservation(reservationData: Omit<Reservation, 'id'>) {
|
||||
return this.request<Reservation>('/reservations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(reservationData),
|
||||
});
|
||||
}
|
||||
|
||||
async updateReservation(id: string, updates: Partial<Reservation>) {
|
||||
return this.request<Reservation>(`/reservations/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
async cancelReservation(id: string) {
|
||||
return this.request<Reservation>(`/reservations/${id}/cancel`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ============ BÚSQUEDA ============
|
||||
|
||||
async search(query: string, filters?: {
|
||||
type?: 'location' | 'poi' | 'business';
|
||||
category?: string;
|
||||
city?: string;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams({ q: query });
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value) searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return this.request<{
|
||||
locations: Location[];
|
||||
pois: PointOfInterest[];
|
||||
businesses: any[];
|
||||
}>(`/search?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
// ============ MÉTODOS DE CONVENIENCIA ============
|
||||
|
||||
// Obtener destinos populares
|
||||
async getPopularDestinations() {
|
||||
return this.getPOIs({
|
||||
page: 1,
|
||||
limit: 6
|
||||
});
|
||||
}
|
||||
|
||||
// Buscar lugares cerca de una ubicación
|
||||
async getNearbyPlaces(latitude: number, longitude: number, radius = 5000) {
|
||||
return this.request<{ data: PointOfInterest[] }>
|
||||
(`/search/nearby?lat=${latitude}&lng=${longitude}&radius=${radius}`);
|
||||
}
|
||||
|
||||
// Obtener estadísticas del dashboard
|
||||
async getDashboardStats() {
|
||||
return this.request<{
|
||||
totalUsers: number;
|
||||
totalReservations: number;
|
||||
totalLocations: number;
|
||||
totalPOIs: number;
|
||||
}>('/dashboard/stats');
|
||||
}
|
||||
}
|
||||
|
||||
// Instancia singleton del servicio API
|
||||
export const karibeoAPI = new KaribeoAPI();
|
||||
|
||||
// Hooks personalizados para React Query (opcional)
|
||||
export const useAuth = () => {
|
||||
return {
|
||||
login: karibeoAPI.login.bind(karibeoAPI),
|
||||
register: karibeoAPI.register.bind(karibeoAPI),
|
||||
logout: () => karibeoAPI.clearToken(),
|
||||
getProfile: karibeoAPI.getProfile.bind(karibeoAPI),
|
||||
};
|
||||
};
|
||||
|
||||
export default karibeoAPI;
|
||||
309
src/services/bookmarkApi.ts
Normal file
309
src/services/bookmarkApi.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
// Mock Bookmark API Service
|
||||
export interface BookmarkItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
rating: number;
|
||||
reviewCount: number;
|
||||
location: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
category: string;
|
||||
price?: string;
|
||||
isVerified: boolean;
|
||||
isFeatured: boolean;
|
||||
amenities: string[];
|
||||
coordinates: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
gallery: string[];
|
||||
openingHours: {
|
||||
day: string;
|
||||
hours: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// Mock data that matches offers from /explore
|
||||
const mockBookmarks: BookmarkItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Luxury Beach Resort Punta Cana",
|
||||
description: "Experience paradise at our exclusive beachfront resort with world-class amenities",
|
||||
image: "/src/assets/punta-cana.jpg",
|
||||
rating: 4.8,
|
||||
reviewCount: 1247,
|
||||
location: "Punta Cana",
|
||||
address: "Playa Bavaro, Punta Cana 23000, Dominican Republic",
|
||||
phone: "+1 809-555-0123",
|
||||
category: "Resort",
|
||||
price: "$299/night",
|
||||
isVerified: true,
|
||||
isFeatured: true,
|
||||
amenities: ["WiFi", "Pool", "Beach Access", "Spa", "Restaurant"],
|
||||
coordinates: { lat: 18.5601, lng: -68.3725 },
|
||||
gallery: [
|
||||
"/src/assets/punta-cana.jpg",
|
||||
"/src/assets/hero-beach.jpg"
|
||||
],
|
||||
openingHours: [
|
||||
{ day: "Monday", hours: "24/7" },
|
||||
{ day: "Tuesday", hours: "24/7" },
|
||||
{ day: "Wednesday", hours: "24/7" },
|
||||
{ day: "Thursday", hours: "24/7" },
|
||||
{ day: "Friday", hours: "24/7" },
|
||||
{ day: "Saturday", hours: "24/7" },
|
||||
{ day: "Sunday", hours: "24/7" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Historic Old San Juan Tour",
|
||||
description: "Discover the rich history and vibrant culture of Old San Juan with expert guides",
|
||||
image: "/src/assets/san-juan.jpg",
|
||||
rating: 4.6,
|
||||
reviewCount: 892,
|
||||
location: "San Juan",
|
||||
address: "Calle del Cristo, Old San Juan, San Juan 00901, Puerto Rico",
|
||||
phone: "+1 787-555-0456",
|
||||
category: "Tour",
|
||||
price: "$45/person",
|
||||
isVerified: true,
|
||||
isFeatured: false,
|
||||
amenities: ["Guide", "Walking Tour", "Historical Sites", "Photography"],
|
||||
coordinates: { lat: 18.4655, lng: -66.1057 },
|
||||
gallery: [
|
||||
"/src/assets/san-juan.jpg",
|
||||
"/src/assets/hero-beach.jpg"
|
||||
],
|
||||
openingHours: [
|
||||
{ day: "Monday", hours: "9:00 AM - 5:00 PM" },
|
||||
{ day: "Tuesday", hours: "9:00 AM - 5:00 PM" },
|
||||
{ day: "Wednesday", hours: "9:00 AM - 5:00 PM" },
|
||||
{ day: "Thursday", hours: "9:00 AM - 5:00 PM" },
|
||||
{ day: "Friday", hours: "9:00 AM - 5:00 PM" },
|
||||
{ day: "Saturday", hours: "9:00 AM - 6:00 PM" },
|
||||
{ day: "Sunday", hours: "10:00 AM - 4:00 PM" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Santo Domingo Colonial Experience",
|
||||
description: "Explore the first city of the Americas with authentic colonial architecture",
|
||||
image: "/src/assets/santo-domingo.jpg",
|
||||
rating: 4.5,
|
||||
reviewCount: 654,
|
||||
location: "Santo Domingo",
|
||||
address: "Zona Colonial, Santo Domingo 10210, Dominican Republic",
|
||||
phone: "+1 809-555-0789",
|
||||
category: "Cultural",
|
||||
price: "$35/person",
|
||||
isVerified: true,
|
||||
isFeatured: true,
|
||||
amenities: ["Guide", "Cultural Sites", "Museums", "Architecture"],
|
||||
coordinates: { lat: 18.4861, lng: -69.9312 },
|
||||
gallery: [
|
||||
"/src/assets/santo-domingo.jpg",
|
||||
"/src/assets/hero-beach.jpg"
|
||||
],
|
||||
openingHours: [
|
||||
{ day: "Monday", hours: "8:00 AM - 6:00 PM" },
|
||||
{ day: "Tuesday", hours: "8:00 AM - 6:00 PM" },
|
||||
{ day: "Wednesday", hours: "8:00 AM - 6:00 PM" },
|
||||
{ day: "Thursday", hours: "8:00 AM - 6:00 PM" },
|
||||
{ day: "Friday", hours: "8:00 AM - 6:00 PM" },
|
||||
{ day: "Saturday", hours: "8:00 AM - 7:00 PM" },
|
||||
{ day: "Sunday", hours: "9:00 AM - 5:00 PM" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Caribbean Culinary Adventure",
|
||||
description: "Taste authentic Caribbean flavors with our premium food tour experience",
|
||||
image: "/src/assets/hero-beach.jpg",
|
||||
rating: 4.7,
|
||||
reviewCount: 1123,
|
||||
location: "Multiple Locations",
|
||||
address: "Various restaurants across the Caribbean",
|
||||
phone: "+1 829-555-0321",
|
||||
category: "Food & Drink",
|
||||
price: "$85/person",
|
||||
isVerified: true,
|
||||
isFeatured: false,
|
||||
amenities: ["Food Tasting", "Local Guide", "Transportation", "Recipes"],
|
||||
coordinates: { lat: 18.2208, lng: -66.5901 },
|
||||
gallery: [
|
||||
"/src/assets/hero-beach.jpg",
|
||||
"/src/assets/punta-cana.jpg"
|
||||
],
|
||||
openingHours: [
|
||||
{ day: "Monday", hours: "11:00 AM - 8:00 PM" },
|
||||
{ day: "Tuesday", hours: "11:00 AM - 8:00 PM" },
|
||||
{ day: "Wednesday", hours: "11:00 AM - 8:00 PM" },
|
||||
{ day: "Thursday", hours: "11:00 AM - 8:00 PM" },
|
||||
{ day: "Friday", hours: "11:00 AM - 9:00 PM" },
|
||||
{ day: "Saturday", hours: "10:00 AM - 9:00 PM" },
|
||||
{ day: "Sunday", hours: "10:00 AM - 8:00 PM" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "Tropical Spa & Wellness Center",
|
||||
description: "Rejuvenate your body and soul with our premium spa treatments",
|
||||
image: "/src/assets/punta-cana.jpg",
|
||||
rating: 4.9,
|
||||
reviewCount: 567,
|
||||
location: "Bavaro",
|
||||
address: "Av. Alemania, Bavaro, Punta Cana 23000, Dominican Republic",
|
||||
phone: "+1 809-555-0654",
|
||||
category: "Wellness",
|
||||
price: "$120/session",
|
||||
isVerified: true,
|
||||
isFeatured: true,
|
||||
amenities: ["Spa", "Massage", "Wellness", "Relaxation", "Beauty"],
|
||||
coordinates: { lat: 18.6821, lng: -68.4645 },
|
||||
gallery: [
|
||||
"/src/assets/punta-cana.jpg",
|
||||
"/src/assets/santo-domingo.jpg"
|
||||
],
|
||||
openingHours: [
|
||||
{ day: "Monday", hours: "8:00 AM - 8:00 PM" },
|
||||
{ day: "Tuesday", hours: "8:00 AM - 8:00 PM" },
|
||||
{ day: "Wednesday", hours: "8:00 AM - 8:00 PM" },
|
||||
{ day: "Thursday", hours: "8:00 AM - 8:00 PM" },
|
||||
{ day: "Friday", hours: "8:00 AM - 9:00 PM" },
|
||||
{ day: "Saturday", hours: "7:00 AM - 9:00 PM" },
|
||||
{ day: "Sunday", hours: "8:00 AM - 7:00 PM" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
title: "Adventure Water Sports Center",
|
||||
description: "Thrilling water activities and equipment rental for all skill levels",
|
||||
image: "/src/assets/san-juan.jpg",
|
||||
rating: 4.4,
|
||||
reviewCount: 890,
|
||||
location: "Boca Chica",
|
||||
address: "Playa Boca Chica, Santo Domingo Este, Dominican Republic",
|
||||
phone: "+1 809-555-0987",
|
||||
category: "Adventure",
|
||||
price: "$60/activity",
|
||||
isVerified: true,
|
||||
isFeatured: false,
|
||||
amenities: ["Water Sports", "Equipment Rental", "Instruction", "Safety Gear"],
|
||||
coordinates: { lat: 18.4484, lng: -69.6113 },
|
||||
gallery: [
|
||||
"/src/assets/san-juan.jpg",
|
||||
"/src/assets/hero-beach.jpg"
|
||||
],
|
||||
openingHours: [
|
||||
{ day: "Monday", hours: "7:00 AM - 6:00 PM" },
|
||||
{ day: "Tuesday", hours: "7:00 AM - 6:00 PM" },
|
||||
{ day: "Wednesday", hours: "7:00 AM - 6:00 PM" },
|
||||
{ day: "Thursday", hours: "7:00 AM - 6:00 PM" },
|
||||
{ day: "Friday", hours: "7:00 AM - 7:00 PM" },
|
||||
{ day: "Saturday", hours: "6:00 AM - 7:00 PM" },
|
||||
{ day: "Sunday", hours: "7:00 AM - 6:00 PM" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
class BookmarkApiService {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
// Use environment variable or fallback to mock
|
||||
this.baseUrl = import.meta.env.VITE_BOOKMARK_API_URL || 'mock';
|
||||
}
|
||||
|
||||
async getBookmarks(userId?: string): Promise<BookmarkItem[]> {
|
||||
try {
|
||||
if (this.baseUrl === 'mock' || import.meta.env.DEV) {
|
||||
// Return mock data for development
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API delay
|
||||
return mockBookmarks;
|
||||
}
|
||||
|
||||
// Real API call would go here
|
||||
const response = await fetch(`${this.baseUrl}/bookmarks?userId=${userId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch bookmarks');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching bookmarks:', error);
|
||||
// Fallback to mock data
|
||||
return mockBookmarks;
|
||||
}
|
||||
}
|
||||
|
||||
async addBookmark(itemId: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
if (this.baseUrl === 'mock' || import.meta.env.DEV) {
|
||||
// Simulate successful bookmark addition
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/bookmarks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ itemId, userId }),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error adding bookmark:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async removeBookmark(itemId: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
if (this.baseUrl === 'mock' || import.meta.env.DEV) {
|
||||
// Simulate successful bookmark removal
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/bookmarks/${itemId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ userId }),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error removing bookmark:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async isBookmarked(itemId: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
if (this.baseUrl === 'mock' || import.meta.env.DEV) {
|
||||
// Simulate checking bookmark status
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return mockBookmarks.some(item => item.id === itemId);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/bookmarks/check?itemId=${itemId}&userId=${userId}`);
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
const result = await response.json();
|
||||
return result.isBookmarked;
|
||||
} catch (error) {
|
||||
console.error('Error checking bookmark status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bookmarkApi = new BookmarkApiService();
|
||||
export { mockBookmarks };
|
||||
189
src/services/chatApi.ts
Normal file
189
src/services/chatApi.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// Chat API Service with configurable endpoints
|
||||
import { mockChatData } from './mockChatData';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
online: boolean;
|
||||
lastSeen?: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
chatId: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
senderAvatar: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
isOwn: boolean;
|
||||
status: 'sent' | 'delivered' | 'read';
|
||||
}
|
||||
|
||||
export interface Chat {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'direct' | 'group';
|
||||
participants: User[];
|
||||
lastMessage?: Message;
|
||||
unreadCount: number;
|
||||
avatar?: string;
|
||||
online?: boolean;
|
||||
lastActivity: string;
|
||||
}
|
||||
|
||||
class ChatApiService {
|
||||
private baseUrl: string;
|
||||
private apiKey: string;
|
||||
private useMockData: boolean;
|
||||
|
||||
constructor() {
|
||||
// In a real environment, these would come from environment variables
|
||||
this.baseUrl = import.meta.env.VITE_CHAT_API_URL || 'http://localhost:3001/api';
|
||||
this.apiKey = import.meta.env.VITE_CHAT_API_KEY || '';
|
||||
this.useMockData = !this.apiKey || import.meta.env.DEV;
|
||||
}
|
||||
|
||||
private async apiCall<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
if (this.useMockData) {
|
||||
// Return mock data
|
||||
return this.getMockData(endpoint) as T;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API call failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private getMockData(endpoint: string): any {
|
||||
// Simulate API responses with mock data
|
||||
const delay = () => new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 500));
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
await delay();
|
||||
|
||||
if (endpoint.includes('/chats')) {
|
||||
resolve(mockChatData.chats);
|
||||
} else if (endpoint.includes('/users')) {
|
||||
resolve(mockChatData.users);
|
||||
} else if (endpoint.includes('/messages')) {
|
||||
const chatId = endpoint.split('/')[2];
|
||||
resolve(mockChatData.messages.filter(m => m.chatId === chatId));
|
||||
} else if (endpoint.includes('/search')) {
|
||||
resolve(mockChatData.users);
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get all chats for current user
|
||||
async getChats(): Promise<Chat[]> {
|
||||
return this.apiCall<Chat[]>('/chats');
|
||||
}
|
||||
|
||||
// Get messages for a specific chat
|
||||
async getMessages(chatId: string): Promise<Message[]> {
|
||||
return this.apiCall<Message[]>(`/chats/${chatId}/messages`);
|
||||
}
|
||||
|
||||
// Send a message
|
||||
async sendMessage(chatId: string, content: string): Promise<Message> {
|
||||
if (this.useMockData) {
|
||||
const newMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
chatId,
|
||||
senderId: 'current-user',
|
||||
senderName: 'You',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content,
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
isOwn: true,
|
||||
status: 'sent'
|
||||
};
|
||||
mockChatData.messages.push(newMessage);
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
return this.apiCall<Message>(`/chats/${chatId}/messages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
}
|
||||
|
||||
// Search users
|
||||
async searchUsers(query: string): Promise<User[]> {
|
||||
if (this.useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return mockChatData.users.filter(user =>
|
||||
user.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
return this.apiCall<User[]>(`/users/search?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
|
||||
// Create a new chat
|
||||
async createChat(participantIds: string[]): Promise<Chat> {
|
||||
if (this.useMockData) {
|
||||
const participants = mockChatData.users.filter(user =>
|
||||
participantIds.includes(user.id)
|
||||
);
|
||||
|
||||
const newChat: Chat = {
|
||||
id: Date.now().toString(),
|
||||
name: participants.length === 1 ? participants[0].name : `Group Chat`,
|
||||
type: participants.length === 1 ? 'direct' : 'group',
|
||||
participants,
|
||||
unreadCount: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
avatar: participants[0]?.avatar,
|
||||
online: participants[0]?.online
|
||||
};
|
||||
|
||||
mockChatData.chats.push(newChat);
|
||||
return newChat;
|
||||
}
|
||||
|
||||
return this.apiCall<Chat>('/chats', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ participantIds }),
|
||||
});
|
||||
}
|
||||
|
||||
// Get online users
|
||||
async getOnlineUsers(): Promise<User[]> {
|
||||
const users = await this.apiCall<User[]>('/users/online');
|
||||
return users.filter(user => user.online);
|
||||
}
|
||||
|
||||
// Mark messages as read
|
||||
async markAsRead(chatId: string): Promise<void> {
|
||||
if (this.useMockData) {
|
||||
// Update mock data
|
||||
const chat = mockChatData.chats.find(c => c.id === chatId);
|
||||
if (chat) {
|
||||
chat.unreadCount = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.apiCall(`/chats/${chatId}/read`, { method: 'POST' });
|
||||
}
|
||||
}
|
||||
|
||||
export const chatApi = new ChatApiService();
|
||||
314
src/services/mockApi.ts
Normal file
314
src/services/mockApi.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
// Mock data for Karibeo API simulation
|
||||
export const mockUsers = [
|
||||
{
|
||||
id: '1',
|
||||
email: 'usuario@karibeo.com',
|
||||
password: '123456',
|
||||
name: 'Juan Pérez',
|
||||
type: 'tourist',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150',
|
||||
location: { lat: 18.4861, lng: -69.9312 }, // Santo Domingo
|
||||
preferences: { language: 'es' }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
email: 'comercio@karibeo.com',
|
||||
password: '123456',
|
||||
name: 'María García',
|
||||
type: 'business',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b829?w=150',
|
||||
location: { lat: 18.5204, lng: -69.9055 }, // Santo Domingo
|
||||
preferences: { language: 'es' }
|
||||
}
|
||||
];
|
||||
|
||||
export const mockListings = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Hotel Colonial Boutique',
|
||||
category: 'hotel',
|
||||
location: { lat: 18.4861, lng: -69.9312, address: 'Zona Colonial, Santo Domingo' },
|
||||
price: 120,
|
||||
rating: 4.8,
|
||||
images: ['https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400'],
|
||||
description: 'Hermoso hotel boutique en el corazón de la Zona Colonial',
|
||||
amenities: ['WiFi', 'Desayuno', 'Piscina', 'Spa'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Restaurante El Malecón',
|
||||
category: 'restaurant',
|
||||
location: { lat: 18.4692, lng: -69.9014, address: 'Malecón, Santo Domingo' },
|
||||
price: 45,
|
||||
rating: 4.6,
|
||||
images: ['https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400'],
|
||||
description: 'Exquisita comida dominicana con vista al mar',
|
||||
amenities: ['Vista al mar', 'Terraza', 'Música en vivo'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Tour Guiado Zona Colonial',
|
||||
category: 'tour',
|
||||
location: { lat: 18.4861, lng: -69.9312, address: 'Plaza de Armas, Santo Domingo' },
|
||||
price: 25,
|
||||
rating: 4.9,
|
||||
images: ['https://images.unsplash.com/photo-1539650116574-75c0c6d73c1e?w=400'],
|
||||
description: 'Descubre la historia de la primera ciudad de América',
|
||||
amenities: ['Guía certificado', 'Grupo pequeño', 'Incluye entradas'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Resort Playa Bávaro',
|
||||
category: 'hotel',
|
||||
location: { lat: 18.6813, lng: -68.4090, address: 'Playa Bávaro, Punta Cana' },
|
||||
price: 280,
|
||||
rating: 4.7,
|
||||
images: ['https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=400'],
|
||||
description: 'Resort todo incluido frente al mar Caribe',
|
||||
amenities: ['Todo incluido', 'Playa privada', 'Spa', 'Golf'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Excursión Isla Saona',
|
||||
category: 'tour',
|
||||
location: { lat: 18.1667, lng: -68.8000, address: 'Isla Saona, La Romana' },
|
||||
price: 85,
|
||||
rating: 4.8,
|
||||
images: ['https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=400'],
|
||||
description: 'Día completo en la paradisíaca Isla Saona',
|
||||
amenities: ['Transporte incluido', 'Almuerzo', 'Snorkel', 'Guía'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Casa Colonial Airbnb',
|
||||
category: 'apartment',
|
||||
location: { lat: 18.4756, lng: -69.9390, address: 'Gazcue, Santo Domingo' },
|
||||
price: 60,
|
||||
rating: 4.5,
|
||||
images: ['https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=400'],
|
||||
description: 'Acogedora casa colonial en zona histórica',
|
||||
amenities: ['WiFi', 'Cocina', 'Aire acondicionado', 'Jardín'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'Restaurante Lucia',
|
||||
category: 'restaurant',
|
||||
location: { lat: 18.4853, lng: -69.9314, address: 'Calle Hostos, Zona Colonial' },
|
||||
price: 65,
|
||||
rating: 4.9,
|
||||
images: ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=400'],
|
||||
description: 'Restaurante gourmet en casa colonial del siglo XVI',
|
||||
amenities: ['Terraza', 'Wine bar', 'Chef internacional', 'Ambiente histórico'],
|
||||
ownerId: '2'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Apartamento Moderno Piantini',
|
||||
category: 'apartment',
|
||||
location: { lat: 18.4736, lng: -69.9506, address: 'Piantini, Santo Domingo' },
|
||||
price: 95,
|
||||
rating: 4.6,
|
||||
images: ['https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=400'],
|
||||
description: 'Moderno apartamento en zona exclusiva',
|
||||
amenities: ['Piscina', 'Gimnasio', 'Seguridad 24h', 'Vista panorámica'],
|
||||
ownerId: '2'
|
||||
}
|
||||
];
|
||||
|
||||
export const mockBookings = [
|
||||
{
|
||||
id: '1',
|
||||
listingId: '1',
|
||||
userId: '1',
|
||||
checkIn: '2024-02-15',
|
||||
checkOut: '2024-02-18',
|
||||
guests: 2,
|
||||
total: 360,
|
||||
status: 'confirmed',
|
||||
paymentMethod: 'paypal'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
listingId: '2',
|
||||
userId: '1',
|
||||
date: '2024-02-20',
|
||||
guests: 4,
|
||||
total: 180,
|
||||
status: 'pending',
|
||||
paymentMethod: 'credit_card'
|
||||
}
|
||||
];
|
||||
|
||||
export const mockWalletData = {
|
||||
balance: 1250.50,
|
||||
transactions: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'income',
|
||||
amount: 120,
|
||||
description: 'Pago por reserva Hotel Colonial',
|
||||
date: '2024-02-10',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'expense',
|
||||
amount: -45,
|
||||
description: 'Comisión plataforma',
|
||||
date: '2024-02-10',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'income',
|
||||
amount: 180,
|
||||
description: 'Pago restaurante El Malecón',
|
||||
date: '2024-02-12',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Simulate API calls with promises
|
||||
export const mockApi = {
|
||||
// Authentication
|
||||
login: async (email: string, password: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
const user = mockUsers.find(u => u.email === email && u.password === password);
|
||||
if (user) {
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
resolve({ user: userWithoutPassword, token: 'mock-jwt-token' });
|
||||
} else {
|
||||
reject(new Error('Credenciales inválidas'));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
register: async (userData: any) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newUser = {
|
||||
id: String(mockUsers.length + 1),
|
||||
...userData,
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150'
|
||||
};
|
||||
mockUsers.push(newUser);
|
||||
const { password: _, ...userWithoutPassword } = newUser;
|
||||
resolve({ user: userWithoutPassword, token: 'mock-jwt-token' });
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
// Listings
|
||||
getListings: async (location?: { lat: number, lng: number }, radius = 10) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
let filteredListings = mockListings;
|
||||
|
||||
if (location) {
|
||||
// Simulate distance filtering (simplified)
|
||||
filteredListings = mockListings.filter(listing => {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(listing.location.lat - location.lat, 2) +
|
||||
Math.pow(listing.location.lng - location.lng, 2)
|
||||
) * 111; // Rough km conversion
|
||||
return distance <= radius;
|
||||
});
|
||||
}
|
||||
|
||||
resolve(filteredListings);
|
||||
}, 800);
|
||||
});
|
||||
},
|
||||
|
||||
getUserListings: async (userId: string) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const userListings = mockListings.filter(listing => listing.ownerId === userId);
|
||||
resolve(userListings);
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
createListing: async (listingData: any) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newListing = {
|
||||
id: String(mockListings.length + 1),
|
||||
...listingData,
|
||||
rating: 0,
|
||||
images: [listingData.image || 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400']
|
||||
};
|
||||
mockListings.push(newListing);
|
||||
resolve(newListing);
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
// Bookings
|
||||
getBookings: async (userId: string) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const userBookings = mockBookings
|
||||
.filter(booking => booking.userId === userId)
|
||||
.map(booking => ({
|
||||
...booking,
|
||||
listing: mockListings.find(l => l.id === booking.listingId)
|
||||
}));
|
||||
resolve(userBookings);
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
createBooking: async (bookingData: any) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newBooking = {
|
||||
id: String(mockBookings.length + 1),
|
||||
...bookingData,
|
||||
status: 'pending'
|
||||
};
|
||||
mockBookings.push(newBooking);
|
||||
resolve(newBooking);
|
||||
}, 800);
|
||||
});
|
||||
},
|
||||
|
||||
// Wallet
|
||||
getWallet: async (userId: string) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockWalletData);
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
// Analytics for dashboard
|
||||
getDashboardStats: async (userId: string) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
totalIncome: 5899,
|
||||
incomeChange: 20.9,
|
||||
visitors: 780192,
|
||||
visitorsChange: 20,
|
||||
totalOrders: 796542,
|
||||
ordersChange: -9.01,
|
||||
recentBookings: mockBookings.slice(0, 5).map(booking => ({
|
||||
...booking,
|
||||
listing: mockListings.find(l => l.id === booking.listingId),
|
||||
user: mockUsers.find(u => u.id === booking.userId)
|
||||
}))
|
||||
});
|
||||
}, 600);
|
||||
});
|
||||
}
|
||||
};
|
||||
387
src/services/mockChatData.ts
Normal file
387
src/services/mockChatData.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
// Mock data for chat system
|
||||
import { User, Chat, Message } from './chatApi';
|
||||
|
||||
export const mockChatData = {
|
||||
users: [
|
||||
{
|
||||
id: 'user-1',
|
||||
name: 'Alexander Kaminski',
|
||||
email: 'alexander@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastSeen: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-2',
|
||||
name: 'Edwin Martins',
|
||||
email: 'edwin@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastSeen: new Date(Date.now() - 3600000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-3',
|
||||
name: 'Gabriel North',
|
||||
email: 'gabriel@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastSeen: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-4',
|
||||
name: 'Ethan Blackwood',
|
||||
email: 'ethan@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastSeen: new Date(Date.now() - 86400000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-5',
|
||||
name: 'Alexander Steele',
|
||||
email: 'alexs@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastSeen: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-6',
|
||||
name: 'Marcus Knight',
|
||||
email: 'marcus@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastSeen: new Date(Date.now() - 1800000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-7',
|
||||
name: 'Pranoti Deshpande',
|
||||
email: 'pranoti@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastSeen: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-8',
|
||||
name: 'Sarah Wilson',
|
||||
email: 'sarah@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastSeen: new Date(Date.now() - 7200000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-9',
|
||||
name: 'Michael Chen',
|
||||
email: 'michael@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastSeen: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'user-10',
|
||||
name: 'Lisa Rodriguez',
|
||||
email: 'lisa@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastSeen: new Date(Date.now() - 3600000).toISOString()
|
||||
}
|
||||
] as User[],
|
||||
|
||||
chats: [
|
||||
{
|
||||
id: 'chat-1',
|
||||
name: 'Alexander Kaminski',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-1',
|
||||
name: 'Alexander Kaminski',
|
||||
email: 'alexander@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true
|
||||
}
|
||||
],
|
||||
unreadCount: 2,
|
||||
lastActivity: new Date(Date.now() - 300000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastMessage: {
|
||||
id: 'msg-1',
|
||||
chatId: 'chat-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'Alexander Kaminski',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'A new feature has been updated to your...',
|
||||
timestamp: '10:30 AM',
|
||||
isOwn: false,
|
||||
status: 'delivered'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chat-2',
|
||||
name: 'Edwin Martins',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-2',
|
||||
name: 'Edwin Martins',
|
||||
email: 'edwin@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false
|
||||
}
|
||||
],
|
||||
unreadCount: 1,
|
||||
lastActivity: new Date(Date.now() - 3600000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastMessage: {
|
||||
id: 'msg-2',
|
||||
chatId: 'chat-2',
|
||||
senderId: 'user-2',
|
||||
senderName: 'Edwin Martins',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'How can i improve my chances of getting a deposit?',
|
||||
timestamp: '10:05 PM',
|
||||
isOwn: false,
|
||||
status: 'delivered'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chat-3',
|
||||
name: 'Gabriel North',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-3',
|
||||
name: 'Gabriel North',
|
||||
email: 'gabriel@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true
|
||||
}
|
||||
],
|
||||
unreadCount: 0,
|
||||
lastActivity: new Date(Date.now() - 86400000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastMessage: {
|
||||
id: 'msg-3',
|
||||
chatId: 'chat-3',
|
||||
senderId: 'user-3',
|
||||
senderName: 'Gabriel North',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'Hey Chris, could i ask you to help me out with variation...',
|
||||
timestamp: 'Tue',
|
||||
isOwn: false,
|
||||
status: 'read'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chat-4',
|
||||
name: 'Ethan Blackwood',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-4',
|
||||
name: 'Ethan Blackwood',
|
||||
email: 'ethan@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false
|
||||
}
|
||||
],
|
||||
unreadCount: 0,
|
||||
lastActivity: new Date(Date.now() - 172800000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastMessage: {
|
||||
id: 'msg-4',
|
||||
chatId: 'chat-4',
|
||||
senderId: 'user-4',
|
||||
senderName: 'Ethan Blackwood',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'By injected humour, or randomised words which...',
|
||||
timestamp: '1/22/2019',
|
||||
isOwn: false,
|
||||
status: 'read'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chat-5',
|
||||
name: 'Alexander Steele',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-5',
|
||||
name: 'Alexander Steele',
|
||||
email: 'alexs@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true
|
||||
}
|
||||
],
|
||||
unreadCount: 3,
|
||||
lastActivity: new Date(Date.now() - 259200000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastMessage: {
|
||||
id: 'msg-5',
|
||||
chatId: 'chat-5',
|
||||
senderId: 'user-5',
|
||||
senderName: 'Alexander Steele',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'No more running out of the office at 4pm on Fridays!',
|
||||
timestamp: '1/18/2019',
|
||||
isOwn: false,
|
||||
status: 'delivered'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chat-6',
|
||||
name: 'Marcus Knight',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-6',
|
||||
name: 'Marcus Knight',
|
||||
email: 'marcus@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false
|
||||
}
|
||||
],
|
||||
unreadCount: 0,
|
||||
lastActivity: new Date(Date.now() - 345600000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: false,
|
||||
lastMessage: {
|
||||
id: 'msg-6',
|
||||
chatId: 'chat-6',
|
||||
senderId: 'user-6',
|
||||
senderName: 'Marcus Knight',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'All your favourite books at your reach! We are now mobile',
|
||||
timestamp: '1/09/2019',
|
||||
isOwn: false,
|
||||
status: 'read'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chat-7',
|
||||
name: 'Pranoti Deshpande',
|
||||
type: 'direct',
|
||||
participants: [
|
||||
{
|
||||
id: 'user-7',
|
||||
name: 'Pranoti Deshpande',
|
||||
email: 'pranoti@example.com',
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true
|
||||
}
|
||||
],
|
||||
unreadCount: 0,
|
||||
lastActivity: new Date(Date.now() - 432000000).toISOString(),
|
||||
avatar: '/api/placeholder/40/40',
|
||||
online: true,
|
||||
lastMessage: {
|
||||
id: 'msg-7',
|
||||
chatId: 'chat-7',
|
||||
senderId: 'user-7',
|
||||
senderName: 'Pranoti Deshpande',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'Dear Deborah, your Thai massage is today at 5pm.',
|
||||
timestamp: 'Feb 9',
|
||||
isOwn: false,
|
||||
status: 'read'
|
||||
}
|
||||
}
|
||||
] as Chat[],
|
||||
|
||||
messages: [
|
||||
// Chat 1 messages
|
||||
{
|
||||
id: 'msg-1-1',
|
||||
chatId: 'chat-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'Alexander Kaminski',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'Hello! How are you doing today?',
|
||||
timestamp: '10:30 AM',
|
||||
isOwn: false,
|
||||
status: 'delivered'
|
||||
},
|
||||
{
|
||||
id: 'msg-1-2',
|
||||
chatId: 'chat-1',
|
||||
senderId: 'current-user',
|
||||
senderName: 'You',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: "Hi! I'm doing great, thank you for asking. How about you?",
|
||||
timestamp: '10:35 AM',
|
||||
isOwn: true,
|
||||
status: 'read'
|
||||
},
|
||||
{
|
||||
id: 'msg-1-3',
|
||||
chatId: 'chat-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'Alexander Kaminski',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'A new feature has been updated to your account. Would you like me to show you around?',
|
||||
timestamp: '10:36 AM',
|
||||
isOwn: false,
|
||||
status: 'delivered'
|
||||
},
|
||||
{
|
||||
id: 'msg-1-4',
|
||||
chatId: 'chat-1',
|
||||
senderId: 'current-user',
|
||||
senderName: 'You',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: "That sounds great! I'd love to hear more about it.",
|
||||
timestamp: '10:40 AM',
|
||||
isOwn: true,
|
||||
status: 'read'
|
||||
},
|
||||
// Chat 2 messages
|
||||
{
|
||||
id: 'msg-2-1',
|
||||
chatId: 'chat-2',
|
||||
senderId: 'user-2',
|
||||
senderName: 'Edwin Martins',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'How can i improve my chances of getting a deposit?',
|
||||
timestamp: '10:05 PM',
|
||||
isOwn: false,
|
||||
status: 'delivered'
|
||||
},
|
||||
{
|
||||
id: 'msg-2-2',
|
||||
chatId: 'chat-2',
|
||||
senderId: 'current-user',
|
||||
senderName: 'You',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'There are several ways to improve your chances. First, make sure your profile is complete...',
|
||||
timestamp: '10:10 PM',
|
||||
isOwn: true,
|
||||
status: 'read'
|
||||
},
|
||||
// Chat 3 messages
|
||||
{
|
||||
id: 'msg-3-1',
|
||||
chatId: 'chat-3',
|
||||
senderId: 'user-3',
|
||||
senderName: 'Gabriel North',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'Hey Chris, could i ask you to help me out with variation of our main design theme?',
|
||||
timestamp: '2:15 PM',
|
||||
isOwn: false,
|
||||
status: 'read'
|
||||
},
|
||||
{
|
||||
id: 'msg-3-2',
|
||||
chatId: 'chat-3',
|
||||
senderId: 'current-user',
|
||||
senderName: 'You',
|
||||
senderAvatar: '/api/placeholder/40/40',
|
||||
content: 'Of course! What specific variations are you looking for?',
|
||||
timestamp: '2:20 PM',
|
||||
isOwn: true,
|
||||
status: 'read'
|
||||
}
|
||||
] as Message[]
|
||||
};
|
||||
Reference in New Issue
Block a user