feat: monorepo structure with Django backend, React frontend, and Expo mobile app

- Backend: Django 5 + DRF with accounts, documents, collections, and reading apps
  - Custom User model with email-based auth, JWT via SimpleJWT
  - Full CRUD viewsets with ModelSerializer + DRF routers
  - pytest, Ruff, drf-spectacular (OpenAPI), whitenoise
  - Dockerfile for production deployment

- Frontend: React 18 + TypeScript + Vite
  - Lazy-loaded routes with ProtectedRoute/PublicRoute guards
  - Auth context with useReducer, token refresh interceptor
  - Pages: Login, Register, Library, Document Detail, Reader, Collections, Settings
  - Dark theme, responsive grid layout, Vite proxy to Django backend

- Mobile: Expo SDK 51 + React Native + Expo Router
  - File-based routing with login, register, and library screens
  - AsyncStorage for token persistence, token refresh interceptor
  - Shared API types via @cloud-reader/shared workspace package

- Shared: TypeScript types (API responses, auth, documents, etc.)
- CI/CD: 3 independent GitHub Actions pipelines (backend, frontend, mobile)
This commit is contained in:
Marko (Hermes Implementer)
2026-05-26 00:51:54 +00:00
commit b8bd1dca14
79 changed files with 3717 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
from rest_framework import serializers
from .models import Document
class DocumentListSerializer(serializers.ModelSerializer[Document]):
cover_url = serializers.SerializerMethodField()
class Meta:
model = Document
fields = [
"id", "title", "author", "cover_url", "description",
"file_type", "file_size", "page_count", "tags",
"is_public", "uploaded_at", "updated_at",
]
read_only_fields = ["id", "uploaded_at", "updated_at"]
def get_cover_url(self, obj: Document) -> str | None:
return obj.cover_url
class DocumentDetailSerializer(DocumentListSerializer):
owner = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta(DocumentListSerializer.Meta):
fields = DocumentListSerializer.Meta.fields + ["owner", "file"]
class DocumentUploadSerializer(serializers.ModelSerializer[Document]):
class Meta:
model = Document
fields = [
"title", "author", "description", "cover", "file",
"file_type", "file_size", "page_count", "tags", "is_public",
]
def validate_file_size(self, value: int) -> int:
max_size = 100 * 1024 * 1024 # 100 MB
if value > max_size:
raise serializers.ValidationError("File size must not exceed 100 MB.")
return value