implement Accept Changes button

Changed files
+277 -18
app
api
change-request
accept
change-request
[uri]
+185
app/api/change-request/accept/route.ts
··· 1 + import { NextRequest, NextResponse } from 'next/server' 2 + import { getSession } from '@/lib/session' 3 + import { getGlobalOAuthClient } from '@/lib/auth/client' 4 + import { Agent } from '@atproto/api' 5 + import { getPdsEndpoint } from '@atproto/common-web' 6 + import { IdResolver } from '@atproto/identity' 7 + import { TID } from '@atproto/common' 8 + 9 + async function getSessionAgent(): Promise<{ agent: Agent; oauthSession: any } | null> { 10 + try { 11 + const session = await getSession() 12 + 13 + if (!session.did) { 14 + return null 15 + } 16 + 17 + const client = await getGlobalOAuthClient() 18 + const oauthSession = await client.restore(session.did) 19 + 20 + if (!oauthSession) { 21 + console.log('OAuth session restoration failed') 22 + return null 23 + } 24 + 25 + const agent = new Agent(oauthSession) 26 + return { agent, oauthSession } 27 + } catch (error) { 28 + console.error('Session restore failed:', error) 29 + return null 30 + } 31 + } 32 + 33 + // POST - Accept a change request and apply the proposed changes 34 + export async function POST(request: NextRequest) { 35 + try { 36 + console.log('POST /api/change-request/accept - Starting request') 37 + 38 + const sessionResult = await getSessionAgent() 39 + if (!sessionResult) { 40 + console.log('No agent available - authentication required') 41 + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) 42 + } 43 + 44 + const { agent, oauthSession } = sessionResult 45 + const body = await request.json() 46 + const { changeRequestUri } = body 47 + 48 + if (!changeRequestUri) { 49 + return NextResponse.json({ error: 'changeRequestUri required' }, { status: 400 }) 50 + } 51 + 52 + // Parse the change request URI to get DID and rkey 53 + const uriParts = changeRequestUri.replace('at://', '').split('/') 54 + const requesterDid = uriParts[0] 55 + const changeRequestRkey = uriParts[2] 56 + 57 + console.log('Fetching change request:', { requesterDid, changeRequestRkey }) 58 + 59 + // Resolve requester's DID to get their PDS 60 + const resolver = new IdResolver() 61 + const requesterDidDoc = await resolver.did.resolve(requesterDid) 62 + 63 + if (!requesterDidDoc) { 64 + return NextResponse.json({ error: 'Could not resolve requester DID' }, { status: 404 }) 65 + } 66 + 67 + const requesterPdsEndpoint = getPdsEndpoint(requesterDidDoc) 68 + if (!requesterPdsEndpoint) { 69 + return NextResponse.json({ error: 'No PDS endpoint found for requester' }, { status: 404 }) 70 + } 71 + 72 + // Create agent for requester's PDS to fetch the change request 73 + const requesterAgent = new Agent({ service: requesterPdsEndpoint }) 74 + 75 + // Fetch the change request record 76 + const changeRequestResponse = await requesterAgent.com.atproto.repo.getRecord({ 77 + repo: requesterDid, 78 + collection: 'org.impactindexer.changeRequest', 79 + rkey: changeRequestRkey 80 + }) 81 + 82 + if (!changeRequestResponse.success) { 83 + return NextResponse.json({ error: 'Change request not found' }, { status: 404 }) 84 + } 85 + 86 + const changeRequestData = changeRequestResponse.data.value as any 87 + 88 + // Verify that the current user is the target (project owner) 89 + if (changeRequestData.targetDid !== agent.assertDid) { 90 + return NextResponse.json({ 91 + error: 'Unauthorized - you can only accept change requests for your own projects' 92 + }, { status: 403 }) 93 + } 94 + 95 + console.log('Fetching proposed record data...') 96 + 97 + // Fetch the proposed record data 98 + const proposedUri = changeRequestData.proposedRecord 99 + const proposedUriParts = proposedUri.replace('at://', '').split('/') 100 + const proposedRkey = proposedUriParts[2] 101 + 102 + const proposedResponse = await requesterAgent.com.atproto.repo.getRecord({ 103 + repo: requesterDid, 104 + collection: 'org.impactindexer.status', 105 + rkey: proposedRkey 106 + }) 107 + 108 + if (!proposedResponse.success) { 109 + return NextResponse.json({ error: 'Proposed record not found' }, { status: 404 }) 110 + } 111 + 112 + const proposedData = proposedResponse.data.value as any 113 + 114 + console.log('Applying proposed changes to project owner record...') 115 + 116 + // Check if the project owner has an existing record 117 + let existingRecord = null 118 + let rkey = TID.nextStr() 119 + 120 + try { 121 + const listResponse = await agent.com.atproto.repo.listRecords({ 122 + repo: agent.assertDid, 123 + collection: 'org.impactindexer.status', 124 + limit: 1 125 + }) 126 + 127 + if (listResponse.data.records.length > 0) { 128 + existingRecord = listResponse.data.records[0] 129 + rkey = existingRecord.uri.split('/').pop() || TID.nextStr() 130 + } 131 + } catch (error) { 132 + console.log('No existing record found, will create new one') 133 + } 134 + 135 + // Create the updated record with proposed data, preserving timestamps appropriately 136 + const now = new Date().toISOString() 137 + const updatedRecord = { 138 + $type: 'org.impactindexer.status', 139 + displayName: proposedData.displayName, 140 + description: proposedData.description, 141 + website: proposedData.website, 142 + fundingReceived: proposedData.fundingReceived, 143 + fundingGivenOut: proposedData.fundingGivenOut, 144 + annualBudget: proposedData.annualBudget, 145 + teamSize: proposedData.teamSize, 146 + sustainableRevenuePercent: proposedData.sustainableRevenuePercent, 147 + categories: proposedData.categories, 148 + impactMetrics: proposedData.impactMetrics, 149 + geographicDistribution: proposedData.geographicDistribution, 150 + createdAt: (existingRecord?.value as any)?.createdAt || now, 151 + updatedAt: now, 152 + } 153 + 154 + console.log('Final record to save:', updatedRecord) 155 + 156 + // Remove undefined fields 157 + Object.keys(updatedRecord).forEach(key => { 158 + if ((updatedRecord as any)[key] === undefined) { 159 + delete (updatedRecord as any)[key] 160 + } 161 + }) 162 + 163 + // Save the updated record to the project owner's repository 164 + const response = await agent.com.atproto.repo.putRecord({ 165 + repo: agent.assertDid, 166 + collection: 'org.impactindexer.status', 167 + rkey, 168 + record: updatedRecord, 169 + validate: false, 170 + }) 171 + 172 + console.log('Successfully updated project owner record') 173 + 174 + return NextResponse.json({ 175 + success: true, 176 + updatedRecordUri: response.data.uri, 177 + updatedRecordCid: response.data.cid, 178 + appliedChanges: updatedRecord 179 + }) 180 + 181 + } catch (error) { 182 + console.error('Accept change request failed:', error) 183 + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) 184 + } 185 + }
+92 -18
app/change-request/[uri]/page.tsx
··· 26 26 const [changeRequest, setChangeRequest] = useState<ChangeRequestData | null>(null) 27 27 const [loading, setLoading] = useState(true) 28 28 const [error, setError] = useState<string | null>(null) 29 + const [accepting, setAccepting] = useState(false) 30 + const [acceptSuccess, setAcceptSuccess] = useState(false) 31 + const [acceptError, setAcceptError] = useState<string | null>(null) 32 + 33 + const acceptChanges = async () => { 34 + if (!changeRequest || accepting) return 35 + 36 + setAccepting(true) 37 + setAcceptError(null) 38 + 39 + try { 40 + const response = await fetch('/api/change-request/accept', { 41 + method: 'POST', 42 + headers: { 43 + 'Content-Type': 'application/json', 44 + }, 45 + body: JSON.stringify({ 46 + changeRequestUri: uri 47 + }) 48 + }) 49 + 50 + if (!response.ok) { 51 + const errorData = await response.json() 52 + throw new Error(errorData.error || 'Failed to accept changes') 53 + } 54 + 55 + const result = await response.json() 56 + console.log('Changes accepted successfully:', result) 57 + setAcceptSuccess(true) 58 + } catch (error) { 59 + console.error('Error accepting changes:', error) 60 + setAcceptError(error instanceof Error ? error.message : 'Failed to accept changes') 61 + } finally { 62 + setAccepting(false) 63 + } 64 + } 29 65 30 66 useEffect(() => { 31 67 const fetchChangeRequest = async () => { ··· 223 259 </div> 224 260 )} 225 261 226 - {/* Future: Action buttons for project owners */} 262 + {/* Action buttons for project owners */} 227 263 <div className="bg-surface border-subtle shadow-card p-6"> 228 264 <h3 className="font-medium text-primary mb-2">Project Owner Actions</h3> 229 - <p className="text-sm text-secondary mb-4"> 230 - As the project owner, you can review the proposed changes and decide whether to accept them. 231 - </p> 232 - <div className="flex space-x-3"> 233 - <button 234 - disabled 235 - className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed" 236 - > 237 - Accept Changes (Coming Soon) 238 - </button> 239 - <button 240 - disabled 241 - className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed" 242 - > 243 - Decline (Coming Soon) 244 - </button> 245 - </div> 265 + 266 + {acceptSuccess ? ( 267 + <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mb-4"> 268 + <h4 className="font-medium text-green-800 dark:text-green-200 mb-1"> 269 + ✅ Changes Accepted Successfully! 270 + </h4> 271 + <p className="text-green-700 dark:text-green-300 text-sm"> 272 + The proposed changes have been applied to your project. Your project information has been updated. 273 + </p> 274 + <Link 275 + href={`/project/${encodeURIComponent(changeRequest?.targetDid || '')}`} 276 + className="inline-flex items-center mt-2 text-sm text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200 transition-colors" 277 + > 278 + View Updated Project 279 + <ExternalLink className="h-4 w-4 ml-1" /> 280 + </Link> 281 + </div> 282 + ) : ( 283 + <> 284 + <p className="text-sm text-secondary mb-4"> 285 + As the project owner, you can review the proposed changes and decide whether to accept them. 286 + </p> 287 + 288 + {acceptError && ( 289 + <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4"> 290 + <h4 className="font-medium text-red-800 dark:text-red-200 mb-1"> 291 + Error Accepting Changes 292 + </h4> 293 + <p className="text-red-700 dark:text-red-300 text-sm"> 294 + {acceptError} 295 + </p> 296 + </div> 297 + )} 298 + 299 + <div className="flex space-x-3"> 300 + <button 301 + onClick={acceptChanges} 302 + disabled={accepting || !changeRequest} 303 + className={`px-4 py-2 rounded-lg transition-colors ${ 304 + accepting || !changeRequest 305 + ? 'bg-surface-hover text-muted cursor-not-allowed' 306 + : 'bg-green-600 hover:bg-green-700 text-white' 307 + }`} 308 + > 309 + {accepting ? 'Accepting...' : 'Accept Changes'} 310 + </button> 311 + <button 312 + disabled 313 + className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed" 314 + > 315 + Decline (Coming Soon) 316 + </button> 317 + </div> 318 + </> 319 + )} 246 320 </div> 247 321 </div> 248 322 )