Skip to content

Commit 89ef3c2

Browse files
committed
feat: implement grantee report submission system - Add report submission form and validation, integrate with GitHub API, add email confirmation, implement error handling
1 parent c881aa9 commit 89ef3c2

14 files changed

Lines changed: 14437 additions & 7653 deletions

.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["next/babel"]
3+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react'
2+
import { render } from '@testing-library/react'
3+
import '@testing-library/jest-dom'
4+
5+
// Mock ReactMarkdown
6+
jest.mock('react-markdown', () => {
7+
return function MockReactMarkdown({ children }: { children: string }) {
8+
return <div data-testid="markdown-content">{children}</div>
9+
}
10+
})
11+
12+
import ReportPreview from '../../components/ReportPreview'
13+
14+
describe('ReportPreview', () => {
15+
it('renders with correct content', () => {
16+
const { getByTestId } = render(
17+
<ReportPreview
18+
project_name="Test Project"
19+
report_number="1"
20+
time_spent="test progress"
21+
next_quarter="test plans"
22+
money_usage="test usage"
23+
help_needed="test help"
24+
/>
25+
)
26+
27+
const content = getByTestId('markdown-content').textContent || ''
28+
29+
// Just verify the key elements we care about
30+
expect(content).toContain('Progress Report # 1')
31+
expect(content).toContain('Use of Funds')
32+
})
33+
})

components/GrantReportForm.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface GrantReportFormProps {
1010
grantDetails: {
1111
project_name: string
1212
issue_number: number
13+
email: string
1314
}
1415
email_hash: string
1516
}
@@ -38,6 +39,7 @@ export default function GrantReportForm({
3839
const [error, setError] = useState<string>()
3940
const [showPreview, setShowPreview] = useState(false)
4041
const [recoveredData, setRecoveredData] = useState(false)
42+
const [loading, setLoading] = useState(false)
4143

4244
const {
4345
register,
@@ -170,29 +172,28 @@ export default function GrantReportForm({
170172
])
171173

172174
const onSubmit = async (data: GrantReportFormData) => {
175+
setLoading(true)
173176
setError(undefined)
174177

175178
try {
176179
const response = await fetchPostJSON('/api/report-bot', {
177180
...data,
178181
issue_number: grantDetails.issue_number,
179-
email_hash,
182+
email: grantDetails.email,
180183
})
181184

182185
if (response.error) {
183186
setError(response.error)
187+
setLoading(false)
184188
return
185189
}
186190

187-
// Clear all saved form data for this grant after successful submission
188-
clearSavedData()
189-
190-
setSubmitted(true)
191+
// Clear saved data
192+
localStorage.removeItem('report_draft')
191193
router.push('/reports/success')
192194
} catch (e) {
193-
setError(
194-
'Error submitting report. Your data has been saved locally. Please try again later.'
195-
)
195+
setError('Failed to submit report. Please try again.')
196+
setLoading(false)
196197
}
197198
}
198199

@@ -511,7 +512,7 @@ export default function GrantReportForm({
511512
review it carefully before submitting.
512513
</p>
513514
<div className="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-md">
514-
<ReportPreview data={watchAllFields} />
515+
<ReportPreview {...watchAllFields} />
515516
</div>
516517
</div>
517518

components/GrantValidationForm.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface ValidationResult {
1414
grant_details: {
1515
project_name: string
1616
issue_number: number
17+
email: string
1718
}
1819
email_hash: string
1920
}
@@ -70,13 +71,17 @@ export default function GrantValidationForm({
7071
grant_details: {
7172
project_name: response.project_name,
7273
issue_number: response.issue_number,
74+
email: response.email,
7375
},
7476
email_hash: response.email_hash,
7577
})
7678
} else if (response.grant_details) {
7779
// Handle old format for backward compatibility
7880
onValidationSuccess({
79-
grant_details: response.grant_details,
81+
grant_details: {
82+
...response.grant_details,
83+
email: response.email,
84+
},
8085
email_hash: response.email_hash,
8186
})
8287
} else {

components/ReportPreview.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react'
2+
import ReactMarkdown from 'react-markdown'
3+
4+
interface ReportPreviewProps {
5+
project_name: string
6+
report_number: string
7+
time_spent: string
8+
next_quarter: string
9+
money_usage: string
10+
help_needed?: string
11+
}
12+
13+
export default function ReportPreview({
14+
project_name,
15+
report_number,
16+
time_spent,
17+
next_quarter,
18+
money_usage,
19+
help_needed,
20+
}: ReportPreviewProps) {
21+
const reportContent = `
22+
# Progress Report # ${report_number} for ${project_name}
23+
24+
## Time Spent & Progress Made
25+
${time_spent}
26+
27+
## Plans for Next Quarter
28+
${next_quarter}
29+
30+
## Use of Funds
31+
${money_usage}
32+
33+
${help_needed ? `## Help or Support Needed\n${help_needed}` : ''}
34+
`
35+
36+
return (
37+
<div className="prose prose-invert mx-auto text-gray-100 prose-headings:text-gray-100">
38+
<ReactMarkdown>{reportContent}</ReactMarkdown>
39+
</div>
40+
)
41+
}

jest.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
testEnvironment: 'jsdom',
3+
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
4+
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
5+
moduleNameMapper: {
6+
'^@/components/(.*)$': '<rootDir>/components/$1',
7+
'^@/pages/(.*)$': '<rootDir>/pages/$1',
8+
'^@/utils/(.*)$': '<rootDir>/utils/$1',
9+
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
10+
},
11+
transform: {
12+
'^.+\\.(js|jsx|ts|tsx|mjs)$': ['babel-jest', { presets: ['next/babel'] }],
13+
},
14+
transformIgnorePatterns: [
15+
'/node_modules/(?!(react-markdown|vfile|vfile-message|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|unist-util-stringify-position|mdast-util-to-string|space-separated-tokens|comma-separated-tokens|hast-util-whitespace|property-information|hast-util-to-jsx-runtime|devlop|unist-util-visit|unist-util-is|unist-util-position|unist-builder|mdast-util-to-hast|mdast-util-definitions|unist-util-generated|unist-util-position|unist-util-visit|mdast-util-to-string)/)',
16+
],
17+
}

jest.setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom'

lib/session.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { IronSessionOptions } from '@/lib/iron-session-compat/next'
2+
import {
3+
GetServerSidePropsContext,
4+
GetServerSidePropsResult,
5+
NextApiHandler,
6+
} from 'next'
7+
import {
8+
withIronSessionApiRoute,
9+
withIronSessionSsr,
10+
} from '@/lib/iron-session-compat/next'
11+
12+
export interface SessionData {
13+
email?: string
14+
email_hash?: string
15+
isLoggedIn: boolean
16+
}
17+
18+
const sessionOptions: IronSessionOptions = {
19+
password:
20+
process.env.SECRET_COOKIE_PASSWORD ||
21+
'complex_password_at_least_32_characters_long',
22+
cookieName: 'opensats_session',
23+
cookieOptions: {
24+
secure: process.env.NODE_ENV === 'production',
25+
},
26+
}
27+
28+
export function withSessionRoute(handler: NextApiHandler) {
29+
return withIronSessionApiRoute(handler, sessionOptions)
30+
}
31+
32+
export function withSessionSsr<
33+
P extends { [key: string]: unknown } = { [key: string]: unknown }
34+
>(
35+
handler: (
36+
context: GetServerSidePropsContext
37+
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
38+
) {
39+
return withIronSessionSsr(handler, sessionOptions)
40+
}
41+
42+
declare module 'iron-session' {
43+
interface IronSessionData extends SessionData {
44+
// Add any additional session properties here
45+
timestamp?: number
46+
}
47+
}

0 commit comments

Comments
 (0)