This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/packages/shared/src/utils.ts
T
Marko (Hermes Implementer) fa82fab44a 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
2026-05-29 02:50:09 +00:00

117 lines
3.1 KiB
TypeScript

/** 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;