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,14 @@
|
||||
{
|
||||
"name": "@cloud-reader/shared",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "~5.7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
@@ -0,0 +1,218 @@
|
||||
/** Core domain types for Cloud Reader — shared across web and mobile */
|
||||
|
||||
// ---- User & Auth ----
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
access: string;
|
||||
refresh: string;
|
||||
}
|
||||
|
||||
export interface LoginPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterPayload {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
password2: string;
|
||||
}
|
||||
|
||||
// ---- Books ----
|
||||
|
||||
export interface Book {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
total_pages: number;
|
||||
cover_image: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface BookSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
total_pages: number;
|
||||
cover_image: string;
|
||||
}
|
||||
|
||||
export interface BookListItem {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
genre: string;
|
||||
reading_status: string;
|
||||
reading_status_display: string;
|
||||
cover_image: string | null;
|
||||
}
|
||||
|
||||
export interface BookDetail {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
genre: string;
|
||||
description: string;
|
||||
reading_status: string;
|
||||
reading_status_display: string;
|
||||
cover_image: string | null;
|
||||
total_pages: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface BookSearchParams {
|
||||
q?: string;
|
||||
genre?: string;
|
||||
author?: string;
|
||||
reading_status?: string;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export const READING_STATUS_OPTIONS = [
|
||||
"to_read",
|
||||
"reading",
|
||||
"finished",
|
||||
"dnf",
|
||||
] as const;
|
||||
|
||||
// ---- E-Books ----
|
||||
|
||||
export interface EBookListItem {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
filename: string;
|
||||
format: string;
|
||||
page_count: number;
|
||||
cover_image: string | null;
|
||||
created_at: string;
|
||||
progress: number | null;
|
||||
}
|
||||
|
||||
export interface EBookDetail {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
filename: string;
|
||||
file_url: string;
|
||||
format: string;
|
||||
page_count: number;
|
||||
file_size: number;
|
||||
metadata_json: Record<string, unknown>;
|
||||
cover_image: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
progress: ReadingProgress | null;
|
||||
}
|
||||
|
||||
export interface ReadingProgress {
|
||||
current_position: number;
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
export interface ReadingSettings {
|
||||
font_size: number;
|
||||
font_style: "sans-serif" | "serif" | "monospace";
|
||||
background_color: string;
|
||||
}
|
||||
|
||||
export interface BookChapter {
|
||||
id: number;
|
||||
title: string;
|
||||
index: number;
|
||||
href: string;
|
||||
children: BookChapter[];
|
||||
}
|
||||
|
||||
export interface TocResponse {
|
||||
chapters: BookChapter[];
|
||||
format: string;
|
||||
page_count: number;
|
||||
}
|
||||
|
||||
export interface ContentResponse {
|
||||
page: number;
|
||||
total_pages: number;
|
||||
content: string;
|
||||
chapter_title: string;
|
||||
format: string;
|
||||
}
|
||||
|
||||
// ---- Bookmarks & Notes ----
|
||||
|
||||
export interface Bookmark {
|
||||
id: string;
|
||||
book: string;
|
||||
book_title: string;
|
||||
page: number;
|
||||
location_text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: string;
|
||||
book: string;
|
||||
book_title: string;
|
||||
page: number;
|
||||
location_text: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateBookmarkPayload {
|
||||
book: string;
|
||||
page: number;
|
||||
location_text?: string;
|
||||
}
|
||||
|
||||
export interface CreateNotePayload {
|
||||
book: string;
|
||||
page: number;
|
||||
location_text?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface UpdateNotePayload {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type AnnotationKind = "bookmark" | "note";
|
||||
|
||||
export interface AnnotationEntry {
|
||||
id: string;
|
||||
kind: AnnotationKind;
|
||||
book_title: string;
|
||||
book_id: string;
|
||||
page: number;
|
||||
location_text: string;
|
||||
content?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ---- Generic API shapes ----
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
detail?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user