Archived
feat: Integrate Expo mobile application into monorepo (#16)
- Add mobile/ directory with Expo React Native project - Create API client with JWT auth and token refresh using AsyncStorage - Implement AuthContext for login/register/logout flow - Add screens: Login, Register, Library, Search, Settings - Set up React Navigation with AuthStack and MainTabs - Create packages/shared/ with shared types and utilities - Add shared validation utilities (email, password strength) - Update root package.json workspaces to include mobile + shared - Add spec document docs/backend/009-expo-integration.md
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/** Shared utility functions for Cloud Reader */
|
||||
|
||||
/**
|
||||
* Format an ISO date string to a human-readable date.
|
||||
*/
|
||||
export function formatDate(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date as a relative time string (e.g., "2h ago", "3d ago").
|
||||
*/
|
||||
export function formatRelativeTime(iso: string): string {
|
||||
const now = Date.now();
|
||||
const then = new Date(iso).getTime();
|
||||
const diffMs = now - then;
|
||||
const seconds = Math.floor(diffMs / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}d ago`;
|
||||
if (hours > 0) return `${hours}h ago`;
|
||||
if (minutes > 0) return `${minutes}m ago`;
|
||||
return "just now";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an email address format.
|
||||
*/
|
||||
export function isValidEmail(email: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Password strength check:
|
||||
* - At least 8 characters
|
||||
* - At least one uppercase letter
|
||||
* - At least one lowercase letter
|
||||
* - At least one digit
|
||||
*/
|
||||
export function isStrongPassword(password: string): boolean {
|
||||
return (
|
||||
password.length >= 8 &&
|
||||
/[A-Z]/.test(password) &&
|
||||
/[a-z]/.test(password) &&
|
||||
/\d/.test(password)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two passwords match.
|
||||
*/
|
||||
export function doPasswordsMatch(password: string, confirm: string): boolean {
|
||||
return password === confirm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly reading status label.
|
||||
*/
|
||||
export function readingStatusLabel(status: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
to_read: "To Read",
|
||||
reading: "Reading",
|
||||
finished: "Finished",
|
||||
dnf: "Did Not Finish",
|
||||
};
|
||||
return labels[status] ?? status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size in bytes to a human-readable string.
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
// ---- API endpoint constants ----
|
||||
|
||||
export const API_ENDPOINTS = {
|
||||
auth: {
|
||||
login: "/api/auth/login/",
|
||||
register: "/api/auth/register/",
|
||||
tokenRefresh: "/api/auth/token/refresh/",
|
||||
profile: "/api/auth/profile/",
|
||||
},
|
||||
books: {
|
||||
list: "/api/books/",
|
||||
detail: (id: number | string) => `/api/books/${id}/`,
|
||||
genres: "/api/books/genres/",
|
||||
authors: "/api/books/authors/",
|
||||
},
|
||||
ebooks: {
|
||||
list: "/api/ebooks/",
|
||||
detail: (id: number) => `/api/ebooks/${id}/`,
|
||||
upload: "/api/ebooks/upload/",
|
||||
toc: (id: number) => `/api/ebooks/${id}/toc/`,
|
||||
content: (id: number, page: number) =>
|
||||
`/api/ebooks/${id}/content/?page=${page}`,
|
||||
progress: (id: number) => `/api/ebooks/${id}/progress/`,
|
||||
settings: (id: number) => `/api/ebooks/${id}/settings/`,
|
||||
},
|
||||
annotations: {
|
||||
list: "/api/annotations/",
|
||||
bookmarks: "/api/annotations/bookmarks/",
|
||||
notes: "/api/annotations/notes/",
|
||||
bookmarkDetail: (id: string) => `/api/annotations/bookmarks/${id}/`,
|
||||
noteDetail: (id: string) => `/api/annotations/notes/${id}/`,
|
||||
},
|
||||
} as const;
|
||||
Reference in New Issue
Block a user