Compare changes

Choose any two refs to compare.

Changed files
+320 -40
app
api
change-request
accept
oauth
callback
project-status
change-request
[uri]
methodology
lib
+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 + }
+5 -4
app/api/oauth/callback/route.ts
··· 24 24 break 25 25 } catch (error) { 26 26 lastError = error 27 - console.error(`OAuth callback attempt ${attempt} failed:`, error.message) 27 + const errorMessage = error instanceof Error ? error.message : String(error) 28 + console.error(`OAuth callback attempt ${attempt} failed:`, errorMessage) 28 29 29 30 // Check if it's a network/socket error worth retrying 30 - const isNetworkError = error.message?.includes('UND_ERR_SOCKET') || 31 - error.message?.includes('fetch failed') || 32 - error.message?.includes('Failed to resolve OAuth server metadata') 31 + const isNetworkError = errorMessage.includes('UND_ERR_SOCKET') || 32 + errorMessage.includes('fetch failed') || 33 + errorMessage.includes('Failed to resolve OAuth server metadata') 33 34 34 35 if (isNetworkError && attempt < 3) { 35 36 console.log(`Network error detected, retrying in ${attempt * 1000}ms...`)
+2
app/api/project-status/route.ts
··· 22 22 geographicDistribution?: string 23 23 createdAt: string 24 24 updatedAt?: string 25 + [key: string]: unknown 25 26 } 26 27 27 28 interface ImpactIndexerChangeRequest { ··· 32 33 reason: string 33 34 createdAt: string 34 35 updatedAt?: string 36 + [key: string]: unknown 35 37 } 36 38 37 39 async function getSessionAgent(): Promise<{ agent: Agent; oauthSession: any } | null> {
+120 -28
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 () => { ··· 169 205 170 206 <div className="bg-surface border-subtle shadow-card p-4"> 171 207 <h3 className="font-medium text-primary mb-2">Proposed Changes</h3> 172 - <Link 173 - href={changeRequest.proposedRecord} 174 - target="_blank" 175 - rel="noopener noreferrer" 176 - className="inline-flex items-center text-sm text-accent hover:text-accent-hover transition-colors" 177 - > 178 - View Proposed Record 179 - <ExternalLink className="h-4 w-4 ml-1" /> 180 - </Link> 208 + {(() => { 209 + // Parse the proposed record URI to extract the DID 210 + const proposedUri = changeRequest.proposedRecord 211 + const proposedDid = proposedUri.replace('at://', '').split('/')[0] 212 + return ( 213 + <Link 214 + href={`/project/${encodeURIComponent(proposedDid)}`} 215 + className="inline-flex items-center text-sm text-accent hover:text-accent-hover transition-colors" 216 + > 217 + View Proposed Changes 218 + <ExternalLink className="h-4 w-4 ml-1" /> 219 + </Link> 220 + ) 221 + })()} 181 222 </div> 182 223 </div> 183 224 ··· 194 235 const originalValue = changeRequest.originalData?.[key] 195 236 const proposedValue = changeRequest.proposedData[key] 196 237 197 - if (originalValue === proposedValue) return null 238 + // Deep comparison for arrays and objects 239 + const valuesAreEqual = () => { 240 + if (originalValue === proposedValue) return true 241 + if (Array.isArray(originalValue) && Array.isArray(proposedValue)) { 242 + return originalValue.length === proposedValue.length && 243 + originalValue.every((item, index) => item === proposedValue[index]) 244 + } 245 + if (typeof originalValue === 'object' && typeof proposedValue === 'object') { 246 + return JSON.stringify(originalValue) === JSON.stringify(proposedValue) 247 + } 248 + return false 249 + } 250 + 251 + if (valuesAreEqual()) return null 198 252 199 253 return ( 200 254 <div key={key} className="border-l-2 border-accent pl-4"> ··· 218 272 </div> 219 273 )} 220 274 221 - {/* Future: Action buttons for project owners */} 275 + {/* Action buttons for project owners */} 222 276 <div className="bg-surface border-subtle shadow-card p-6"> 223 277 <h3 className="font-medium text-primary mb-2">Project Owner Actions</h3> 224 - <p className="text-sm text-secondary mb-4"> 225 - As the project owner, you can review the proposed changes and decide whether to accept them. 226 - </p> 227 - <div className="flex space-x-3"> 228 - <button 229 - disabled 230 - className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed" 231 - > 232 - Accept Changes (Coming Soon) 233 - </button> 234 - <button 235 - disabled 236 - className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed" 237 - > 238 - Decline (Coming Soon) 239 - </button> 240 - </div> 278 + 279 + {acceptSuccess ? ( 280 + <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mb-4"> 281 + <h4 className="font-medium text-green-800 dark:text-green-200 mb-1"> 282 + โœ… Changes Accepted Successfully! 283 + </h4> 284 + <p className="text-green-700 dark:text-green-300 text-sm"> 285 + The proposed changes have been applied to your project. Your project information has been updated. 286 + </p> 287 + <Link 288 + href={`/project/${encodeURIComponent(changeRequest?.targetDid || '')}`} 289 + 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" 290 + > 291 + View Updated Project 292 + <ExternalLink className="h-4 w-4 ml-1" /> 293 + </Link> 294 + </div> 295 + ) : ( 296 + <> 297 + <p className="text-sm text-secondary mb-4"> 298 + As the project owner, you can review the proposed changes and decide whether to accept them. 299 + </p> 300 + 301 + {acceptError && ( 302 + <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4"> 303 + <h4 className="font-medium text-red-800 dark:text-red-200 mb-1"> 304 + Error Accepting Changes 305 + </h4> 306 + <p className="text-red-700 dark:text-red-300 text-sm"> 307 + {acceptError} 308 + </p> 309 + </div> 310 + )} 311 + 312 + <div className="flex space-x-3"> 313 + <button 314 + onClick={acceptChanges} 315 + disabled={accepting || !changeRequest} 316 + className={`px-4 py-2 rounded-lg transition-colors ${ 317 + accepting || !changeRequest 318 + ? 'bg-surface-hover text-muted cursor-not-allowed' 319 + : 'bg-green-600 hover:bg-green-700 text-white' 320 + }`} 321 + > 322 + {accepting ? 'Accepting...' : 'Accept Changes'} 323 + </button> 324 + <button 325 + disabled 326 + className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed" 327 + > 328 + Decline (Coming Soon) 329 + </button> 330 + </div> 331 + </> 332 + )} 241 333 </div> 242 334 </div> 243 335 )
+1 -1
app/methodology/page.tsx
··· 207 207 </li> 208 208 <li className="flex items-start"> 209 209 <CheckCircle className="h-5 w-5 text-accent mr-3 mt-0.5 flex-shrink-0" /> 210 - <span>Grant registry databases (Gitcoin, web3 Foundation, etc.)</span> 210 + <span>Grant registry databases (Gitcoin, Ethereum Foundation, etc.)</span> 211 211 </li> 212 212 </ul> 213 213 </div>
+5 -5
lib/data.ts
··· 26 26 ], 27 27 categories: ['Developer Ecosystem', 'Public Goods'], 28 28 fundingHistory: [ 29 - { date: '2024-01', amount: 2500000, type: 'grant', source: 'web3 Foundation' }, 29 + { date: '2024-01', amount: 2500000, type: 'grant', source: 'Ethereum Foundation' }, 30 30 { date: '2023-06', amount: 4200000, type: 'grant', source: 'Gitcoin' }, 31 31 { date: '2023-01', amount: 6060000, type: 'grant', source: 'Community Donations' }, 32 32 ] ··· 59 59 fundingHistory: [ 60 60 { date: '2024-01', amount: 12000000, type: 'revenue', source: 'Platform Fees' }, 61 61 { date: '2023-06', amount: 18000000, type: 'investment', source: 'Series A' }, 62 - { date: '2023-01', amount: 20000000, type: 'grant', source: 'web3 Foundation' }, 62 + { date: '2023-01', amount: 20000000, type: 'grant', source: 'Ethereum Foundation' }, 63 63 ] 64 64 }, 65 65 { ··· 88 88 categories: ['Public Goods', 'Community'], 89 89 fundingHistory: [ 90 90 { date: '2024-01', amount: 450000, type: 'revenue', source: 'Platform Revenue' }, 91 - { date: '2023-08', amount: 2100000, type: 'grant', source: 'web3 Foundation' }, 91 + { date: '2023-08', amount: 2100000, type: 'grant', source: 'Ethereum Foundation' }, 92 92 { date: '2023-03', amount: 5950000, type: 'grant', source: 'Community Donations' }, 93 93 ] 94 94 }, ··· 178 178 categories: ['UBI', 'Community', 'Financial Inclusion'], 179 179 fundingHistory: [ 180 180 { date: '2024-01', amount: 450000, type: 'revenue', source: 'Registration Fees' }, 181 - { date: '2023-10', amount: 3350000, type: 'grant', source: 'web3 Foundation' }, 181 + { date: '2023-10', amount: 3350000, type: 'grant', source: 'Ethereum Foundation' }, 182 182 { date: '2023-04', amount: 5000000, type: 'grant', source: 'Gitcoin Grants' }, 183 183 ] 184 184 }, 185 185 { 186 186 id: 'web3-foundation', 187 - name: 'web3 Foundation', 187 + name: 'Ethereum Foundation', 188 188 logo: '/projects/ef.png', 189 189 description: 'Supporting web3 ecosystem research, development, and community growth', 190 190 website: 'https://web3.foundation/',
+2 -2
package-lock.json
··· 1 1 { 2 - "name": "web3-impact-index", 2 + "name": "interplanetary-impact-index", 3 3 "version": "0.1.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 - "name": "web3-impact-index", 8 + "name": "interplanetary-impact-index", 9 9 "version": "0.1.0", 10 10 "dependencies": { 11 11 "@atproto/api": "^0.16.7",