Archived
Backend: - Create hermes Django app with models: ReadingGroup, GroupBook, Section, ReadingSchedule, MemberProgress - EPUB section splitting service with automatic detection and reading time estimation - Section recommendation engine for 4-week meeting schedule - REST API endpoints for groups, books, sections, schedule, and member progress - Manual section adjustment (merge/split) support Frontend: - GroupsPage: list/create reading groups - GroupDetailPage: manage members, upload EPUB to group, view group books - GroupBookPage: section breakdown with merge/split controls, reading schedule, member progress - API client and TypeScript types for all group operations - i18n keys for English and Spanish Shared: - Group-related types and API endpoint constants in packages/shared
127 lines
3.8 KiB
TypeScript
127 lines
3.8 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}/`,
|
|
},
|
|
groups: {
|
|
list: "/api/groups/",
|
|
detail: (id: number) => `/api/groups/${id}/`,
|
|
books: (groupId: number) => `/api/groups/${groupId}/books/`,
|
|
bookDetail: (groupId: number, bookId: number) => `/api/groups/${groupId}/books/${bookId}/`,
|
|
detectSections: (groupId: number, bookId: number) => `/api/groups/${groupId}/books/${bookId}/detect-sections/`,
|
|
adjustSections: (groupId: number, bookId: number) => `/api/groups/${groupId}/books/${bookId}/adjust-sections/`,
|
|
schedule: (groupId: number, bookId: number) => `/api/groups/${groupId}/books/${bookId}/schedule/`,
|
|
progress: (groupId: number, bookId: number) => `/api/groups/${groupId}/books/${bookId}/progress/`,
|
|
},
|
|
} as const; |