+4
-1
README.md
+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
-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
+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
+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
+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
+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
+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
+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
+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',