mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
- Introduced Vite as the new build tool for the web frontend, enhancing performance and development speed. - Added essential components including landing, login, and admin pages. - Implemented Zustand for state management and integrated error boundaries for improved error handling. - Established a unified API client and validation schemas for consistent data handling. - Created comprehensive test setups and initial tests for core functionalities.
10 KiB
10 KiB
🎯 Simplified Vite Architecture: Essential Simplicity
📊 Current State Analysis
Current API Client Patterns
// Current: Mixed approaches across the app
// 1. TanStack Query + GraphQL Request
const { data } = useQuery({
queryKey: ['subscriptions'],
queryFn: async () => {
const response = await gqlFetcher(GQL_GET_SUBSCRIPTIONS, variables)
return response.subscriptions.subscriptions
},
})
// 2. SWR + GraphQL Request
const { data, error, mutate } = useSWR(
[query, variables],
makeGqlFetcher(query, variables),
{}
)
// 3. Custom hooks with localStorage persistence
const [currentTheme, setCurrentTheme] = usePersistedState({
key: 'theme',
initialValue: 'Light',
})
Current State Management
- TanStack Query: For server state (caching, background refetch)
- SWR: For some queries (inconsistent pattern)
- usePersistedState: For client state with localStorage
- React Context: For theme and global state
- localStorage: Direct access for auth tokens
Current Routing
- Next.js App Router: File-based routing
- No admin routes: Currently no dedicated admin interface
- Protected routes: Handled via
useGetViewerhook
🚀 Simplified Vite Architecture
Single App, Single Port (3000)
packages/web-vite/
├── src/
│ ├── components/ # All components
│ │ ├── auth/ # Auth components
│ │ ├── library/ # Library components
│ │ ├── reader/ # Reader components
│ │ ├── settings/ # Settings components
│ │ └── admin/ # Admin components (protected)
│ ├── hooks/ # Custom hooks
│ │ ├── useAuth.ts # Auth state & actions
│ │ ├── useTheme.ts # Theme management
│ │ └── useLibrary.ts # Library operations
│ ├── services/ # API services
│ │ ├── api-client.ts # Unified GraphQL client
│ │ ├── auth-service.ts # Auth operations
│ │ └── library-service.ts # Library operations
│ ├── stores/ # Global state
│ │ ├── auth-store.ts # Auth state (Zustand)
│ │ └── theme-store.ts # Theme state (Zustand)
│ ├── pages/ # Route components
│ │ ├── LoginPage.tsx
│ │ ├── LibraryPage.tsx
│ │ ├── ReaderPage.tsx
│ │ ├── SettingsPage.tsx
│ │ └── AdminPage.tsx # Protected admin route
│ ├── App.tsx # Main app component
│ └── main.tsx # Entry point
├── vite.config.ts
└── package.json
Unified API Client
// src/services/api-client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
class OmnivoreApiClient {
private apolloClient: ApolloClient<any>
constructor() {
const httpLink = createHttpLink({
uri: `${import.meta.env.VITE_API_URL}/api/graphql`,
})
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('authToken')
return {
headers: {
...headers,
'X-OmnivoreClient': 'web',
...(token && { Authorization: `Bearer ${token}` }),
},
}
})
this.apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
libraryItems: {
merge: false, // Replace instead of merge for pagination
},
},
},
},
}),
})
}
getApolloClient() {
return this.apolloClient
}
// REST API methods
async login(email: string, password: string) {
const response = await fetch(
`${import.meta.env.VITE_API_URL}/api/v2/auth/login`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}
)
return response.json()
}
}
export const apiClient = new OmnivoreApiClient()
Simplified State Management
// src/stores/auth-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface AuthState {
user: User | null
token: string | null
isAuthenticated: boolean
login: (email: string, password: string) => Promise<void>
logout: () => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email: string, password: string) => {
const data = await apiClient.login(email, password)
if (data.success) {
set({
user: data.user,
token: data.accessToken,
isAuthenticated: true,
})
}
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false })
localStorage.removeItem('authToken')
},
}),
{
name: 'omnivore-auth',
storage: localStorage,
}
)
)
Protected Admin Route
// src/components/AdminPage.tsx
import { useAuthStore } from '../stores/auth-store'
import { Navigate } from 'react-router-dom'
export function AdminPage() {
const { user, isAuthenticated } = useAuthStore()
// Simple role-based protection
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
if (user?.role !== 'admin') {
return <Navigate to="/library" replace />
}
return (
<div>
<h1>Admin Dashboard</h1>
{/* Admin content */}
</div>
)
}
React Router Setup
// src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ApolloProvider } from '@apollo/client'
import { useAuthStore } from './stores/auth-store'
import { LoginPage } from './pages/LoginPage'
import { LibraryPage } from './pages/LibraryPage'
import { ReaderPage } from './pages/ReaderPage'
import { SettingsPage } from './pages/SettingsPage'
import { AdminPage } from './pages/AdminPage'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<ApolloProvider client={apiClient.getApolloClient()}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/library" element={<LibraryPage />} />
<Route path="/reader/:id" element={<ReaderPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/" element={<Navigate to="/library" replace />} />
</Routes>
</BrowserRouter>
</ApolloProvider>
</QueryClientProvider>
)
}
export default App
🔄 Migration Strategy: Keep It Simple
Phase 1: Foundation (Week 1)
# Day 1-2: Setup
mkdir packages/web-vite
cd packages/web-vite
npm create vite@latest . -- --template react-ts
# Install dependencies
npm install @tanstack/react-query @apollo/client zustand react-router-dom
npm install @radix-ui/react-* @stitches/react
# Day 3-4: Core Services
# - Create unified API client
# - Set up auth store
# - Create basic routing
Phase 2: Feature Migration (Week 2)
# Day 1-3: Core Features
# - Migrate authentication
# - Migrate library management
# - Migrate article reading
# Day 4-5: Additional Features
# - Migrate settings
# - Add admin interface (if needed)
# - Migrate theme management
Phase 3: Polish (Week 3)
# Day 1-2: Performance
# - Bundle optimization
# - Code splitting
# - Lazy loading
# Day 3-5: Testing & Deploy
# - Unit tests
# - E2E tests
# - Production deployment
🎯 Why Single App Architecture?
Arguments FOR Single App
- ✅ Simpler deployment: One build, one deploy
- ✅ Shared state: All components share the same stores
- ✅ Consistent routing: Single router configuration
- ✅ Easier development: No context switching between apps
- ✅ Current pattern: Matches existing Next.js structure
Arguments AGAINST Multiple Apps
- ❌ Complexity: Multiple builds, deployments, configurations
- ❌ State sharing: Harder to share state between apps
- ❌ Development overhead: Multiple dev servers, ports
- ❌ No clear benefit: Admin is just a protected route
Admin Interface: Protected Route
// Current: No admin interface exists
// Proposed: Simple protected route at /admin
// Benefits:
// - Same codebase, same deployment
// - Shared components and state
// - Simple role-based access control
// - Easy to maintain and extend
📊 Simplified Benefits
Immediate Benefits
- ✅ 50-100x faster development (Vite vs Next.js)
- ✅ Unified API client (no more mixed SWR/Query patterns)
- ✅ Consistent state management (Zustand + TanStack Query)
- ✅ Simple routing (React Router)
- ✅ Single deployment (one build, one app)
Development Benefits
- ✅ Easier debugging (single app context)
- ✅ Shared components (no duplication)
- ✅ Consistent patterns (unified approach)
- ✅ Faster iteration (no context switching)
Maintenance Benefits
- ✅ Single codebase (easier to maintain)
- ✅ Unified testing (single test suite)
- ✅ Consistent deployment (one pipeline)
- ✅ Shared dependencies (no version conflicts)
🚀 Implementation Plan
Week 1: Foundation
- Set up Vite with React Router
- Create unified API client
- Implement auth store with Zustand
- Set up basic routing structure
Week 2: Core Migration
- Migrate authentication flow
- Migrate library management
- Migrate article reading
- Add admin interface (if needed)
Week 3: Polish & Deploy
- Performance optimization
- Testing and quality assurance
- Production deployment
- Documentation
This simplified approach gives you all the performance benefits of Vite while maintaining the simplicity of a single application architecture.