the statusphere demo reworked into a vite/react app in a monorepo

ok try fix serving

Changed files
+53 -39
packages
appview
client
src
components
hooks
pages
services
+4 -1
README.md
··· 63 63 For production deployment: 64 64 65 65 1. Build all packages in the correct order: 66 + 66 67 ```bash 67 68 pnpm build 68 69 ``` 69 - 70 + 70 71 This will: 72 + 71 73 - Build the lexicon package first (shared type definitions) 72 74 - Build the frontend (`packages/client`) next 73 75 - Finally build the backend (`packages/appview`) ··· 78 80 ``` 79 81 80 82 The backend server will: 83 + 81 84 - Serve the API at `/api/*` endpoints 82 85 - Serve the frontend static files from the client's build directory 83 86 - Handle client-side routing by serving index.html for all non-API routes
-3
package.json
··· 11 11 "dev:client": "pnpm --filter @statusphere/client dev", 12 12 "dev:oauth": "node scripts/setup-ngrok.js", 13 13 "lexgen": "pnpm --filter @statusphere/lexicon build", 14 - 15 14 "build": "pnpm build:lexicon && pnpm build:client && pnpm build:appview", 16 15 "build:lexicon": "pnpm --filter @statusphere/lexicon build", 17 16 "build:appview": "pnpm --filter @statusphere/appview build", 18 17 "build:client": "pnpm --filter @statusphere/client build", 19 - 20 18 "start": "pnpm --filter @statusphere/appview start", 21 19 "start:dev": "pnpm -r start", 22 20 "start:appview": "pnpm --filter @statusphere/appview start", 23 21 "start:client": "pnpm --filter @statusphere/client start", 24 - 25 22 "clean": "pnpm -r clean", 26 23 "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", 27 24 "typecheck": "pnpm -r typecheck"
+13 -12
packages/appview/src/index.ts
··· 1 1 import events from 'node:events' 2 + import fs from 'node:fs' 2 3 import type http from 'node:http' 3 4 import path from 'node:path' 4 5 import type { OAuthClient } from '@atproto/oauth-client-node' ··· 18 19 import { createIngester } from '#/ingester' 19 20 import { env } from '#/lib/env' 20 21 import { createRouter } from '#/routes' 21 - import fs from 'node:fs' 22 22 23 23 // Application state passed to the router and elsewhere 24 24 export type AppContext = { ··· 121 121 const router = createRouter(ctx) 122 122 app.use(express.json()) 123 123 app.use(express.urlencoded({ extended: true })) 124 - 125 - // API routes 124 + 125 + // Two versions of the API routes: 126 + // 1. Mounted at /api for the client 126 127 app.use('/api', router) 127 - 128 + 128 129 // Serve static files from the frontend build 129 130 const frontendPath = path.resolve(__dirname, '../../../client/dist') 130 - 131 + 131 132 // Check if the frontend build exists 132 133 if (fs.existsSync(frontendPath)) { 133 134 logger.info(`Serving frontend static files from: ${frontendPath}`) 134 - 135 + 135 136 // Serve static files 136 137 app.use(express.static(frontendPath)) 137 - 138 + 138 139 // For any other requests, send the index.html file 139 140 app.get('*', (req, res) => { 140 - // Skip API routes 141 - if (req.path.startsWith('/api/')) { 142 - return res.sendStatus(404) 141 + // Only handle non-API paths 142 + if (!req.path.startsWith('/api/')) { 143 + res.sendFile(path.join(frontendPath, 'index.html')) 144 + } else { 145 + res.status(404).json({ error: 'API endpoint not found' }) 143 146 } 144 - 145 - res.sendFile(path.join(frontendPath, 'index.html')) 146 147 }) 147 148 } else { 148 149 logger.warn(`Frontend build not found at: ${frontendPath}`)
+9
packages/appview/src/routes.ts
··· 117 117 clientSession.did = session.did 118 118 await clientSession.save() 119 119 120 + // Get the origin and determine appropriate redirect 121 + const host = req.get('host') || '' 122 + const protocol = req.protocol || 'http' 123 + const baseUrl = `${protocol}://${host}` 124 + 125 + ctx.logger.info( 126 + `OAuth callback successful, redirecting to ${baseUrl}/oauth-callback`, 127 + ) 128 + 120 129 // Redirect to the frontend oauth-callback page 121 130 res.redirect('/oauth-callback') 122 131 } catch (err) {
+5 -3
packages/client/src/components/StatusForm.tsx
··· 160 160 transition-all duration-200 161 161 ${isSelected ? 'opacity-60' : 'opacity-100'} 162 162 ${!isSelected ? 'hover:bg-gray-100 dark:hover:bg-gray-700 hover:scale-110' : ''} 163 - ${isCurrentStatus 164 - ? 'bg-blue-50 ring-1 ring-blue-200 dark:bg-blue-900 dark:bg-opacity-30 dark:ring-blue-700' 165 - : ''} 163 + ${ 164 + isCurrentStatus 165 + ? 'bg-blue-50 ring-1 ring-blue-200 dark:bg-blue-900 dark:bg-opacity-30 dark:ring-blue-700' 166 + : '' 167 + } 166 168 active:scale-95 167 169 focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 168 170 `}
+6 -2
packages/client/src/components/StatusList.tsx
··· 18 18 19 19 if (isLoading && !data) { 20 20 return ( 21 - <div className="py-4 text-center text-gray-500 dark:text-gray-400">Loading statuses...</div> 21 + <div className="py-4 text-center text-gray-500 dark:text-gray-400"> 22 + Loading statuses... 23 + </div> 22 24 ) 23 25 } 24 26 ··· 32 34 33 35 if (statuses.length === 0) { 34 36 return ( 35 - <div className="py-4 text-center text-gray-500 dark:text-gray-400">No statuses yet.</div> 37 + <div className="py-4 text-center text-gray-500 dark:text-gray-400"> 38 + No statuses yet. 39 + </div> 36 40 ) 37 41 } 38 42
+3 -3
packages/client/src/hooks/useAuth.tsx
··· 77 77 try { 78 78 // Add a small artificial delay for UX purposes 79 79 const loginPromise = api.login(handle) 80 - 80 + 81 81 // Ensure the loading state shows for at least 800ms for better UX 82 82 const result = await Promise.all([ 83 83 loginPromise, 84 - new Promise(resolve => setTimeout(resolve, 800)) 84 + new Promise((resolve) => setTimeout(resolve, 800)), 85 85 ]).then(([loginResult]) => loginResult) 86 - 86 + 87 87 return result 88 88 } catch (err) { 89 89 const message = err instanceof Error ? err.message : 'Login failed'
+6 -2
packages/client/src/pages/HomePage.tsx
··· 13 13 <h2 className="text-2xl font-semibold mb-2 text-gray-800 dark:text-gray-200"> 14 14 Loading Statusphere... 15 15 </h2> 16 - <p className="text-gray-600 dark:text-gray-400">Setting up your experience</p> 16 + <p className="text-gray-600 dark:text-gray-400"> 17 + Setting up your experience 18 + </p> 17 19 </div> 18 20 </div> 19 21 ) ··· 23 25 return ( 24 26 <div className="flex justify-center items-center py-16"> 25 27 <div className="text-center p-6 max-w-md"> 26 - <h2 className="text-2xl font-semibold mb-2 text-gray-800 dark:text-gray-200">Error</h2> 28 + <h2 className="text-2xl font-semibold mb-2 text-gray-800 dark:text-gray-200"> 29 + Error 30 + </h2> 27 31 <p className="text-red-500 mb-4">{error}</p> 28 32 <a 29 33 href="/login"
+7 -13
packages/client/src/services/api.ts
··· 1 1 import { AppBskyActorDefs, XyzStatusphereDefs } from '@statusphere/lexicon' 2 2 3 - const API_URL = import.meta.env.VITE_API_URL || '/api' 3 + // Use '/api' prefix consistently for all API calls 4 + const API_URL = '/api' 4 5 5 6 // Helper function for logging API actions 6 7 function logApiCall( ··· 24 25 25 26 // API service 26 27 export const api = { 27 - // Get base URL 28 - getBaseUrl() { 29 - return API_URL || '' 30 - }, 31 28 // Login 32 29 async login(handle: string) { 33 - const url = API_URL ? `${API_URL}/login` : '/login' 30 + const url = `${API_URL}/login` 34 31 logApiCall('POST', url) 35 32 36 33 const response = await fetch(url, { ··· 52 49 53 50 // Logout 54 51 async logout() { 55 - const url = API_URL ? `${API_URL}/logout` : '/logout' 52 + const url = `${API_URL}/logout` 56 53 logApiCall('POST', url) 57 54 const response = await fetch(url, { 58 55 method: 'POST', ··· 68 65 69 66 // Get current user 70 67 async getCurrentUser() { 71 - const url = API_URL ? `${API_URL}/user` : '/user' 68 + const url = `${API_URL}/user` 72 69 logApiCall('GET', url) 73 70 try { 74 - console.log('📞 Fetching user from:', url, 'with credentials included') 75 - // Debug output - what headers are we sending? 76 71 const headers = { 77 72 Accept: 'application/json', 78 73 } 79 - console.log('📨 Request headers:', headers) 80 74 81 75 const response = await fetch(url, { 82 76 credentials: 'include', // This is crucial for sending cookies ··· 120 114 121 115 // Get statuses 122 116 async getStatuses() { 123 - const url = API_URL ? `${API_URL}/statuses` : '/statuses' 117 + const url = `${API_URL}/statuses` 124 118 logApiCall('GET', url) 125 119 const response = await fetch(url, { 126 120 credentials: 'include', ··· 137 131 138 132 // Create status 139 133 async createStatus(status: string) { 140 - const url = API_URL ? `${API_URL}/status` : '/status' 134 + const url = `${API_URL}/status` 141 135 logApiCall('POST', url) 142 136 const response = await fetch(url, { 143 137 method: 'POST',