Add comprehensive .gitignore

This commit is contained in:
2025-10-01 19:01:52 +00:00
commit f3ff07c0ef
254 changed files with 88254 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
import { trackingApi } from '../tracking'
import apiClient from '../client'
jest.mock('../client')
const mockedApiClient = apiClient as jest.Mocked<typeof apiClient>
describe('trackingApi', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('createActivity', () => {
it('transforms frontend data to backend format', async () => {
const mockActivity = {
id: 'act_123',
childId: 'chd_456',
type: 'feeding',
startedAt: '2024-01-01T12:00:00Z',
metadata: { amount: 120, type: 'bottle' },
loggedBy: 'usr_789',
createdAt: '2024-01-01T12:00:00Z',
}
mockedApiClient.post.mockResolvedValue({
data: { data: { activity: mockActivity } },
} as any)
const result = await trackingApi.createActivity('chd_456', {
type: 'feeding',
timestamp: '2024-01-01T12:00:00Z',
data: { amount: 120, type: 'bottle' },
})
expect(mockedApiClient.post).toHaveBeenCalledWith(
'/api/v1/activities?childId=chd_456',
{
type: 'feeding',
startedAt: '2024-01-01T12:00:00Z',
metadata: { amount: 120, type: 'bottle' },
notes: undefined,
}
)
expect(result).toEqual({
...mockActivity,
timestamp: mockActivity.startedAt,
data: mockActivity.metadata,
})
})
})
describe('getActivities', () => {
it('transforms backend data to frontend format', async () => {
const mockActivities = [
{
id: 'act_123',
childId: 'chd_456',
type: 'feeding',
startedAt: '2024-01-01T12:00:00Z',
metadata: { amount: 120 },
},
{
id: 'act_124',
childId: 'chd_456',
type: 'sleep',
startedAt: '2024-01-01T14:00:00Z',
metadata: { duration: 120 },
},
]
mockedApiClient.get.mockResolvedValue({
data: { data: { activities: mockActivities } },
} as any)
const result = await trackingApi.getActivities('chd_456', 'feeding')
expect(mockedApiClient.get).toHaveBeenCalledWith('/api/v1/activities', {
params: { childId: 'chd_456', type: 'feeding' },
})
expect(result).toEqual([
{
id: 'act_123',
childId: 'chd_456',
type: 'feeding',
startedAt: '2024-01-01T12:00:00Z',
metadata: { amount: 120 },
timestamp: '2024-01-01T12:00:00Z',
data: { amount: 120 },
},
{
id: 'act_124',
childId: 'chd_456',
type: 'sleep',
startedAt: '2024-01-01T14:00:00Z',
metadata: { duration: 120 },
timestamp: '2024-01-01T14:00:00Z',
data: { duration: 120 },
},
])
})
})
})

View File

@@ -0,0 +1,60 @@
import apiClient from './client';
export interface Child {
id: string;
familyId: string;
name: string;
birthDate: string;
gender: 'male' | 'female' | 'other';
photoUrl?: string;
medicalInfo?: any;
createdAt: string;
}
export interface CreateChildData {
name: string;
birthDate: string;
gender: 'male' | 'female' | 'other';
photoUrl?: string;
medicalInfo?: any;
}
export interface UpdateChildData extends Partial<CreateChildData> {}
export const childrenApi = {
// Get all children for the authenticated user
getChildren: async (familyId?: string): Promise<Child[]> => {
const params = familyId ? { familyId } : {};
const response = await apiClient.get('/api/v1/children', { params });
return response.data.data.children;
},
// Get a specific child
getChild: async (id: string): Promise<Child> => {
const response = await apiClient.get(`/api/v1/children/${id}`);
return response.data.data.child;
},
// Create a new child
createChild: async (familyId: string, data: CreateChildData): Promise<Child> => {
const response = await apiClient.post(`/api/v1/children?familyId=${familyId}`, data);
return response.data.data.child;
},
// Update a child
updateChild: async (id: string, data: UpdateChildData): Promise<Child> => {
const response = await apiClient.patch(`/api/v1/children/${id}`, data);
return response.data.data.child;
},
// Delete a child
deleteChild: async (id: string): Promise<void> => {
await apiClient.delete(`/api/v1/children/${id}`);
},
// Get child's age
getChildAge: async (id: string): Promise<{ ageInMonths: number; ageInYears: number; remainingMonths: number }> => {
const response = await apiClient.get(`/api/v1/children/${id}/age`);
return response.data.data;
},
};

View File

@@ -0,0 +1,101 @@
import axios from 'axios';
import { tokenStorage } from '@/lib/utils/tokenStorage';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
// Request interceptor to add auth token
apiClient.interceptors.request.use(
(config) => {
const token = tokenStorage.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Only handle token refresh on client side
if (typeof window === 'undefined') {
return Promise.reject(error);
}
// If error is 401 and we haven't tried to refresh yet
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = tokenStorage.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token');
}
const response = await axios.post(
`${API_BASE_URL}/api/v1/auth/refresh`,
{ refreshToken },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true
}
);
// Handle different response structures
let newAccessToken;
let newRefreshToken;
if (response.data?.data?.tokens?.accessToken) {
newAccessToken = response.data.data.tokens.accessToken;
newRefreshToken = response.data.data.tokens.refreshToken;
} else if (response.data?.tokens?.accessToken) {
newAccessToken = response.data.tokens.accessToken;
newRefreshToken = response.data.tokens.refreshToken;
} else if (response.data?.accessToken) {
newAccessToken = response.data.accessToken;
newRefreshToken = response.data.refreshToken;
} else {
throw new Error('Invalid token refresh response');
}
// Update tokens in storage
tokenStorage.setAccessToken(newAccessToken);
if (newRefreshToken) {
tokenStorage.setRefreshToken(newRefreshToken);
}
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
console.error('Token refresh failed:', refreshError);
// Refresh failed, clear tokens and redirect to login
tokenStorage.clearTokens();
// Avoid redirect loop - only redirect if not already on login page
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login';
}
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default apiClient;

View File

@@ -0,0 +1,69 @@
import apiClient from './client';
export interface Family {
id: string;
name: string;
shareCode: string;
createdBy: string;
subscriptionTier: string;
members?: FamilyMember[];
}
export interface FamilyMember {
id: string;
userId: string;
familyId: string;
role: 'parent' | 'caregiver' | 'viewer';
permissions: any;
user?: {
id: string;
name: string;
email: string;
};
}
export interface InviteMemberData {
email: string;
role: 'parent' | 'caregiver' | 'viewer';
}
export interface JoinFamilyData {
shareCode: string;
}
export const familiesApi = {
// Get a specific family
getFamily: async (familyId: string): Promise<Family> => {
const response = await apiClient.get(`/api/v1/families/${familyId}`);
return response.data.data.family;
},
// Get family members
getFamilyMembers: async (familyId: string): Promise<FamilyMember[]> => {
const response = await apiClient.get(`/api/v1/families/${familyId}/members`);
return response.data.data.members;
},
// Invite a family member
inviteMember: async (familyId: string, data: InviteMemberData): Promise<any> => {
const response = await apiClient.post(`/api/v1/families/invite?familyId=${familyId}`, data);
return response.data.data.invitation;
},
// Join a family using share code
joinFamily: async (data: JoinFamilyData): Promise<FamilyMember> => {
const response = await apiClient.post('/api/v1/families/join', data);
return response.data.data.member;
},
// Update member role
updateMemberRole: async (familyId: string, userId: string, role: string): Promise<FamilyMember> => {
const response = await apiClient.patch(`/api/v1/families/${familyId}/members/${userId}/role`, { role });
return response.data.data.member;
},
// Remove a family member
removeMember: async (familyId: string, userId: string): Promise<void> => {
await apiClient.delete(`/api/v1/families/${familyId}/members/${userId}`);
},
};

View File

@@ -0,0 +1,105 @@
import apiClient from './client';
export type ActivityType = 'feeding' | 'sleep' | 'diaper' | 'medication' | 'milestone' | 'note';
export interface Activity {
id: string;
childId: string;
type: ActivityType;
timestamp: string;
data: any;
notes?: string;
loggedBy: string;
createdAt: string;
}
export interface CreateActivityData {
type: ActivityType;
timestamp: string;
data: any;
notes?: string;
}
export interface UpdateActivityData extends Partial<CreateActivityData> {}
export interface DailySummary {
date: string;
feedingCount: number;
sleepTotalMinutes: number;
diaperCount: number;
activities: Activity[];
}
export const trackingApi = {
// Get all activities for a child
getActivities: async (
childId: string,
type?: ActivityType,
startDate?: string,
endDate?: string
): Promise<Activity[]> => {
const params: any = { childId };
if (type) params.type = type;
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
const response = await apiClient.get('/api/v1/activities', { params });
// Transform backend response to frontend format
const activities = response.data.data.activities.map((activity: any) => ({
...activity,
timestamp: activity.startedAt, // Frontend expects timestamp
data: activity.metadata, // Frontend expects data
}));
return activities;
},
// Get a specific activity
getActivity: async (id: string): Promise<Activity> => {
const response = await apiClient.get(`/api/v1/activities/${id}`);
const activity = response.data.data.activity;
// Transform backend response to frontend format
return {
...activity,
timestamp: activity.startedAt,
data: activity.metadata,
};
},
// Create a new activity
createActivity: async (childId: string, data: CreateActivityData): Promise<Activity> => {
// Transform frontend data structure to backend DTO format
const payload = {
type: data.type,
startedAt: data.timestamp, // Backend expects startedAt, not timestamp
metadata: data.data, // Backend expects metadata, not data
notes: data.notes,
};
const response = await apiClient.post(`/api/v1/activities?childId=${childId}`, payload);
const activity = response.data.data.activity;
// Transform backend response to frontend format
return {
...activity,
timestamp: activity.startedAt,
data: activity.metadata,
};
},
// Update an activity
updateActivity: async (id: string, data: UpdateActivityData): Promise<Activity> => {
const response = await apiClient.patch(`/api/v1/activities/${id}`, data);
return response.data.data.activity;
},
// Delete an activity
deleteActivity: async (id: string): Promise<void> => {
await apiClient.delete(`/api/v1/activities/${id}`);
},
// Get daily summary
getDailySummary: async (childId: string, date: string): Promise<DailySummary> => {
const response = await apiClient.get('/api/v1/activities/daily-summary', {
params: { childId, date },
});
return response.data.data;
},
};

View File

@@ -0,0 +1,31 @@
import apiClient from './client';
export interface UserPreferences {
notifications?: boolean;
emailUpdates?: boolean;
darkMode?: boolean;
}
export interface UpdateProfileData {
name?: string;
preferences?: UserPreferences;
}
export interface UserProfile {
id: string;
email: string;
name: string;
role: string;
locale: string;
emailVerified: boolean;
preferences?: UserPreferences;
families?: string[];
}
export const usersApi = {
// Update user profile
updateProfile: async (data: UpdateProfileData): Promise<UserProfile> => {
const response = await apiClient.patch('/api/v1/auth/profile', data);
return response.data.data;
},
};