mobile

Shared Code Strategy for React Native

This document outlines what code can be shared between the web (Next.js) and mobile (React Native) apps, and how to structure it.

Shared Packages Structure

packages/
├── shared/
│   ├── types/
│   │   ├── journal.ts          # Journal entry types
│   │   ├── user.ts             # User and profile types
│   │   ├── notebook.ts         # Notebook types
│   │   ├── api.ts              # API request/response types
│   │   └── index.ts            # Re-exports
│   ├── utils/
│   │   ├── dateUtils.ts        # Date formatting (date-fns)
│   │   ├── moonPhase.ts        # Moon phase calculations
│   │   ├── timeUtils.ts        # Time-of-day utilities
│   │   └── validation.ts       # Zod schemas
│   ├── constants/
│   │   ├── fonts.ts            # Font names and mappings
│   │   └── colors.ts           # Color constants
│   └── package.json
└── ui/                         # (Optional) React Native Web components
    └── package.json

Shared Types

Journal Entry Types

// packages/shared/types/journal.ts
export interface JournalEntry {
  id: string
  user_id: string
  title: string | null
  content: string
  mood: string | null
  tags: string[]
  notebook_id: string | null
  font?: string | null
  font_size?: number | null
  background_image?: string | null
  created_at: string
  updated_at: string
  notebooks?: Notebook | null
}

export interface CreateEntryRequest {
  title?: string
  content: string
  mood?: string
  tags?: string[]
  notebook_id?: string | null
  font?: string
  font_size?: number
}

export interface UpdateEntryRequest extends Partial<CreateEntryRequest> {
  id: string
}

User Types

// packages/shared/types/user.ts
export interface UserProfile {
  id: string
  full_name: string | null
  avatar_url: string | null
  preferences: UserPreferences
  role?: 'user' | 'admin' | 'super_admin'
  created_at: string
  updated_at: string
}

export interface UserPreferences {
  theme?: 'light' | 'dark' | 'system'
  default_font?: string
  default_font_size?: number
  [key: string]: any
}

Notebook Types

// packages/shared/types/notebook.ts
export interface Notebook {
  id: string
  user_id: string
  name: string
  cover_image: string | null
  color: string
  font?: string | null
  font_size?: number | null
  created_at: string
  updated_at: string
}

API Types

// packages/shared/types/api.ts
export interface ApiResponse<T> {
  data?: T
  error?: string
  message?: string
}

export interface PaginatedResponse<T> {
  items: T[]
  page: number
  limit: number
  total: number
  hasMore: boolean
}

Shared Utilities

Date Utilities

// packages/shared/utils/dateUtils.ts
import { format, formatDistanceToNow, isToday, isYesterday } from 'date-fns'

export function formatRelativeDate(date: Date | string): string {
  const dateObj = typeof date === 'string' ? new Date(date) : date
  
  if (isToday(dateObj)) {
    return formatDistanceToNow(dateObj, { addSuffix: true })
  }
  
  if (isYesterday(dateObj)) {
    return 'Yesterday'
  }
  
  return formatDistanceToNow(dateObj, { addSuffix: true })
}

export function formatAbsoluteDate(date: Date | string): string {
  const dateObj = typeof date === 'string' ? new Date(date) : date
  return format(dateObj, 'MMMM d, yyyy')
}

Moon Phase Utilities

// packages/shared/utils/moonPhase.ts
export function getMoonPhase(date: Date): number {
  // Known new moon date: January 11, 2024 11:57 UTC
  const knownNewMoon = new Date('2024-01-11T11:57:00Z').getTime()
  const lunarCycle = 29.53059 * 24 * 60 * 60 * 1000
  const dateTime = date.getTime()
  const daysSinceNewMoon = (dateTime - knownNewMoon) % lunarCycle
  const phase = daysSinceNewMoon / lunarCycle
  return phase < 0 ? phase + 1 : phase
}

export function getMoonPhaseIcon(phase: number): string {
  if (phase < 0.03 || phase >= 0.97) return '🌑'
  if (phase < 0.22) return '🌒'
  if (phase < 0.28) return '🌓'
  if (phase < 0.47) return '🌔'
  if (phase < 0.53) return '🌕'
  if (phase < 0.72) return '🌖'
  if (phase < 0.78) return '🌗'
  return '🌘'
}

Time Utilities

// packages/shared/utils/timeUtils.ts
export function isAfterSunset(): boolean {
  const now = new Date()
  const hour = now.getHours()
  return hour >= 18 // 6 PM
}

export function getTimeCircle(date: Date): number {
  const hours = date.getHours()
  const minutes = date.getMinutes()
  let minutesSince3am = (hours * 60 + minutes) - (3 * 60)
  if (minutesSince3am < 0) {
    minutesSince3am += 24 * 60
  }
  const minutesPerCircle = (24 * 60) / 7
  const circle = Math.floor(minutesSince3am / minutesPerCircle)
  return Math.min(6, Math.max(0, circle))
}

Font Utilities

// packages/shared/utils/fonts.ts
export const AVAILABLE_FONTS = [
  'PremiumUltra1',
  'PremiumUltra2',
  'PremiumUltra3',
  'PremiumUltra4',
  'PremiumUltra5',
  'PremiumUltra26',
] as const

export type FontName = typeof AVAILABLE_FONTS[number]

export function resolveFont(font: string | null | undefined, fallback?: string): string {
  if (!font) return fallback || 'system'
  return AVAILABLE_FONTS.includes(font as FontName) ? font : fallback || 'system'
}

export function resolveFontSize(size: number | null | undefined, fallback: number = 16): number {
  return size && size > 0 ? size : fallback
}

Validation Schemas

// packages/shared/utils/validation.ts
import { z } from 'zod'

export const createEntrySchema = z.object({
  title: z.string().optional(),
  content: z.string().min(1, 'Content is required'),
  mood: z.string().optional(),
  tags: z.array(z.string()).optional(),
  notebook_id: z.string().uuid().nullable().optional(),
  font: z.string().optional(),
  font_size: z.number().positive().optional(),
})

export const updateEntrySchema = createEntrySchema.partial().extend({
  id: z.string().uuid(),
})

Setting Up Shared Package

1. Create Package Structure

mkdir -p packages/shared/{types,utils,constants}
cd packages/shared
npm init -y

2. Update package.json

{
  "name": "@hivejournal/shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "date-fns": "^2.30.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "typescript": "^5.3.3"
  }
}

3. Create tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4. Move Source Files

mkdir -p src/{types,utils,constants}
# Move type files to src/types/
# Move utility files to src/utils/
# Move constant files to src/constants/

5. Create Index File

// packages/shared/src/index.ts
export * from './types'
export * from './utils'
export * from './constants'

Using Shared Package

In Web App (Next.js)

// apps/frontend/package.json
{
  "dependencies": {
    "@hivejournal/shared": "workspace:*"
  }
}

// Usage
import { JournalEntry, formatRelativeDate } from '@hivejournal/shared'

In Mobile App (React Native)

// apps/mobile/package.json
{
  "dependencies": {
    "@hivejournal/shared": "workspace:*"
  }
}

// Usage
import { JournalEntry, formatRelativeDate } from '@hivejournal/shared'

Platform-Specific Adaptations

What CAN'T Be Shared

  1. UI Components: Web uses HTML/CSS, React Native uses View/Text
  2. Navigation: Next.js uses file-based routing, RN uses React Navigation
  3. Image Handling: Next.js Image vs React Native Image
  4. Storage: localStorage vs AsyncStorage
  5. Platform APIs: Browser APIs vs React Native APIs

What CAN Be Shared

  1. Type Definitions: 100% shareable
  2. Business Logic: Calculations, transformations
  3. Validation Schemas: Zod schemas work everywhere
  4. Date Utilities: date-fns works in both
  5. Constants: Font names, colors, etc.
  6. API Types: Request/response interfaces

Migration Strategy

Phase 1: Extract Types

  1. Move all TypeScript interfaces to shared package
  2. Update imports in web app
  3. Use in mobile app from day one

Phase 2: Extract Utilities

  1. Move date formatting, moon phase, etc.
  2. Test in web app
  3. Use in mobile app

Phase 3: Extract Constants

  1. Move font lists, color constants
  2. Ensure consistency across platforms

Example: Shared Entry Card Logic

While the UI components can't be shared, the data transformation logic can:

// packages/shared/utils/entryUtils.ts
import { JournalEntry } from '../types'
import { formatRelativeDate, getMoonPhaseIcon, getTimeCircle } from './'

export function getEntryDisplayData(entry: JournalEntry) {
  const createdAt = new Date(entry.created_at)
  return {
    relativeDate: formatRelativeDate(createdAt),
    absoluteDate: formatAbsoluteDate(createdAt),
    moonPhase: getMoonPhaseIcon(getMoonPhase(createdAt)),
    timeCircle: getTimeCircle(createdAt),
    hasCustomBackground: !!entry.background_image,
  }
}

Then use in both platforms:

// Web
const displayData = getEntryDisplayData(entry)
return <div>{displayData.relativeDate}</div>

// Mobile
const displayData = getEntryDisplayData(entry)
return <Text>{displayData.relativeDate}</Text>

Benefits

  1. Type Safety: Single source of truth for types
  2. Consistency: Same logic across platforms
  3. Maintainability: Update once, works everywhere
  4. Testing: Test utilities once, use everywhere
  5. Developer Experience: Autocomplete and types work in both apps

Next Steps

  1. Create packages/shared structure
  2. Move types from web app
  3. Extract utilities
  4. Set up build process
  5. Use in mobile app from the start
REACT NATIVE SHARED CODE — Docs | HiveJournal