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

ok try fix serving

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