+32
-15
app/api/profile/route.ts
+32
-15
app/api/profile/route.ts
···
3
3
import { getGlobalOAuthClient } from '@/lib/auth/client'
4
4
import { Agent } from '@atproto/api'
5
5
6
-
export async function GET() {
6
+
export async function GET(request: Request) {
7
7
try {
8
+
const { searchParams } = new URL(request.url)
9
+
const did = searchParams.get('did')
10
+
8
11
const session = await getSession()
9
12
10
-
if (!session.did) {
11
-
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
13
+
// If a specific DID is requested, allow unauthenticated access for read-only
14
+
const targetDid = did || session.did
15
+
16
+
if (!targetDid) {
17
+
return NextResponse.json({ error: 'DID parameter required or authentication needed' }, { status: 400 })
12
18
}
13
19
14
20
const client = await getGlobalOAuthClient()
15
21
16
-
// Restore the OAuth session
17
-
const oauthSession = await client.restore(session.did)
22
+
// Try to use authenticated session if available and valid
23
+
let agent: Agent
24
+
let usingAuthenticatedAccess = false
18
25
19
-
if (!oauthSession) {
20
-
// Clear invalid session
21
-
await clearSession()
22
-
return NextResponse.json({ error: 'Session expired' }, { status: 401 })
26
+
if (session.did && !did) {
27
+
// Only use OAuth for own profile access
28
+
const oauthSession = await client.restore(session.did)
29
+
30
+
if (oauthSession) {
31
+
agent = new Agent(oauthSession)
32
+
usingAuthenticatedAccess = true
33
+
} else {
34
+
// Clear invalid session
35
+
await clearSession()
36
+
return NextResponse.json({ error: 'Session expired' }, { status: 401 })
37
+
}
38
+
} else {
39
+
// For public access or specific DID lookup, use public API
40
+
agent = new Agent({ service: 'https://public.api.bsky.app' })
41
+
// Note: No authentication needed for public profile access via public API
23
42
}
24
-
25
-
// Create agent and fetch profile using the getProfile method
26
-
const agent = new Agent(oauthSession)
27
43
28
-
// Use the simpler getProfile method like green_globe app
29
-
const profileResponse = await agent.getProfile({ actor: session.did })
44
+
// Use the simpler getProfile method
45
+
const profileResponse = await agent.getProfile({ actor: targetDid })
30
46
31
47
if (!profileResponse.success) {
32
48
return NextResponse.json({ error: 'Profile not found' }, { status: 404 })
···
35
51
const profile = profileResponse.data
36
52
37
53
return NextResponse.json({
38
-
did: session.did,
54
+
did: targetDid,
39
55
handle: profile.handle,
40
56
displayName: profile.displayName,
41
57
avatar: profile.avatar,
···
43
59
followersCount: profile.followersCount,
44
60
followsCount: profile.followsCount,
45
61
postsCount: profile.postsCount,
62
+
isOwner: usingAuthenticatedAccess && session.did === targetDid, // Indicate if the authenticated user owns this profile
46
63
})
47
64
} catch (error) {
48
65
console.error('Profile fetch failed:', error)
+53
-16
app/project/[id]/page.tsx
+53
-16
app/project/[id]/page.tsx
···
3
3
import { useParams } from 'next/navigation'
4
4
import { useEffect, useState } from 'react'
5
5
import Link from 'next/link'
6
-
import { ArrowLeft, ExternalLink, Users, DollarSign, TrendingUp, MapPin, Plus, Edit2, Save, X } from 'lucide-react'
6
+
import { ArrowLeft, ExternalLink, Users, DollarSign, TrendingUp, MapPin, Plus, Edit2, Save, X, Copy, Check } from 'lucide-react'
7
7
import { projects, formatCurrency } from '@/lib/data'
8
8
import FundingChart from '@/components/FundingChart'
9
9
···
11
11
const params = useParams()
12
12
const id = decodeURIComponent(params.id as string)
13
13
const [profile, setProfile] = useState<any>(null)
14
-
const [userSession, setUserSession] = useState<any>(null)
15
14
const [loading, setLoading] = useState(true)
16
15
const [editingField, setEditingField] = useState<string | null>(null)
17
16
const [editValues, setEditValues] = useState<any>({})
17
+
const [copiedDID, setCopiedDID] = useState(false)
18
18
19
19
// Check if this is a DID
20
20
const isDID = id.startsWith('did:plc:')
···
23
23
const staticProject = isDID ? null : projects.find(p => p.id === id)
24
24
25
25
// Check if this is the user's own project
26
-
const isOwnProject = isDID && userSession?.did === id
26
+
const isOwnProject = isDID && profile?.isOwner
27
27
28
28
useEffect(() => {
29
29
const fetchData = async () => {
30
30
try {
31
-
// Check current user session
32
-
const sessionResponse = await fetch('/api/status')
33
-
if (sessionResponse.ok) {
34
-
const sessionData = await sessionResponse.json()
35
-
setUserSession(sessionData)
36
-
}
37
31
38
32
if (isDID) {
39
-
const profileResponse = await fetch('/api/profile')
33
+
const profileResponse = await fetch(`/api/profile?did=${encodeURIComponent(id)}`)
40
34
if (profileResponse.ok) {
41
35
const profileData = await profileResponse.json()
42
36
setProfile(profileData)
···
71
65
setEditingField(null)
72
66
}
73
67
68
+
const handleCopyDID = async () => {
69
+
if (isDID && id) {
70
+
try {
71
+
await navigator.clipboard.writeText(id)
72
+
setCopiedDID(true)
73
+
setTimeout(() => setCopiedDID(false), 2000)
74
+
} catch (err) {
75
+
console.error('Failed to copy DID:', err)
76
+
}
77
+
}
78
+
}
79
+
80
+
const truncateDID = (did: string) => {
81
+
// Extract the part after "did:plc:"
82
+
const didPart = did.replace('did:plc:', '')
83
+
if (didPart.length <= 8) return did // If the DID part is short, show full DID
84
+
85
+
// Show first 4 letters after "plc:" and last 4 letters
86
+
const firstFour = didPart.substring(0, 4)
87
+
const lastFour = didPart.substring(didPart.length - 4)
88
+
return `did:plc:${firstFour}...${lastFour}`
89
+
}
90
+
74
91
if (!isDID && !staticProject) {
75
92
return <div>Project not found</div>
76
93
}
···
107
124
<div className="bg-surface border-subtle shadow-card p-4 sm:p-6 group">
108
125
<div className="flex flex-col sm:flex-row sm:items-start justify-between mb-4 sm:mb-6 gap-4">
109
126
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-3 sm:space-y-0 sm:space-x-4">
110
-
<div className="h-12 w-12 bg-surface-hover flex items-center justify-center flex-shrink-0">
127
+
<div className="h-20 w-20 bg-surface-hover flex items-center justify-center flex-shrink-0">
111
128
{isDID && profile?.avatar ? (
112
-
<img
113
-
src={profile.avatar}
129
+
<img
130
+
src={profile.avatar}
114
131
alt={displayName}
115
-
className="h-12 w-12 rounded object-cover"
132
+
className="h-20 w-20 rounded object-cover"
116
133
/>
117
134
) : (
118
-
<span className="text-lg font-medium text-secondary">
135
+
<span className="text-xl font-medium text-secondary">
119
136
{(project?.name || displayName).substring(0, 2).toUpperCase()}
120
137
</span>
121
138
)}
···
148
165
) : (
149
166
<>
150
167
<h1 className="text-2xl sm:text-3xl font-serif font-semibold text-primary">
151
-
{editValues.name || project?.name || `${displayName}'s Environmental Project`}
168
+
{editValues.name || project?.name || `${displayName}'s project`}
152
169
</h1>
153
170
{isOwnProject && (
154
171
<button
···
257
274
</div>
258
275
)}
259
276
</div>
277
+
278
+
{/* DID Display */}
279
+
{isDID && (
280
+
<div className="mt-2">
281
+
<button
282
+
onClick={handleCopyDID}
283
+
className="inline-flex items-center text-xs sm:text-sm text-muted hover:text-primary transition-colors group"
284
+
title={`Click to copy: ${id}`}
285
+
>
286
+
<span className="font-mono bg-surface-hover px-2 py-1 rounded text-xs sm:text-sm mr-2 truncate">
287
+
{truncateDID(id)}
288
+
</span>
289
+
{copiedDID ? (
290
+
<Check className="h-3 w-3 sm:h-4 sm:w-4 text-accent flex-shrink-0" />
291
+
) : (
292
+
<Copy className="h-3 w-3 sm:h-4 sm:w-4 opacity-60 group-hover:opacity-100 flex-shrink-0" />
293
+
)}
294
+
</button>
295
+
</div>
296
+
)}
260
297
</div>
261
298
</div>
262
299
</div>
+2
-10
components/Sidebar.tsx
+2
-10
components/Sidebar.tsx
···
68
68
<Link href="/about" className="text-sm font-medium text-secondary hover:text-primary transition-colors">
69
69
About
70
70
</Link>
71
-
<Link href="/submit" className="text-sm font-medium text-secondary hover:text-primary transition-colors">
72
-
Submit Project
73
-
</Link>
71
+
74
72
</div>
75
73
76
74
{/* Profile and Controls */}
···
136
134
>
137
135
About
138
136
</Link>
139
-
<Link
140
-
href="/submit"
141
-
className="text-sm font-medium text-secondary hover:text-primary transition-colors py-2"
142
-
onClick={() => setIsMobileMenuOpen(false)}
143
-
>
144
-
Submit Project
145
-
</Link>
137
+
146
138
</div>
147
139
</div>
148
140
</div>