Skip to content

Commit df39fda

Browse files
authored
App router (#1988)
1 parent fadfd4c commit df39fda

93 files changed

Lines changed: 1091 additions & 1136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ module.exports = {
7777
'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }],
7878
'react/jsx-filename-extension': ['error', { extensions: ['.js', '.tsx'] }],
7979
'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }],
80+
'react/no-unescaped-entities': 'off',
8081
'react/jsx-no-target-blank': 'off', // browsers protect against this vulnerability now
8182
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
8283
'react/jsx-one-expression-per-line': 'off',
@@ -284,18 +285,13 @@ module.exports = {
284285
},
285286
},
286287
{
287-
files: ['./pages/api/**/*.ts'],
288+
files: ['./app/api/**/*.ts'],
288289
rules: {
289290
'no-console': 'off',
290291
},
291292
},
292293
{
293-
files: [
294-
'pages/**.js',
295-
'components/head.js',
296-
'components/nav.js',
297-
'components/Timeline/historyData.js',
298-
],
294+
files: ['components/nav.js', 'components/Timeline/historyData.js'],
299295
rules: {
300296
'react/react-in-jsx-scope': 'off',
301297
},

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ out
7272

7373
# storybook build output
7474
.storybook-dist
75+
.storybook-static
76+
storybook-static
7577

7678
# WebStorm Config
7779
.idea
@@ -97,3 +99,5 @@ tsconfig.tsbuildinfo
9799

98100
# MCP config (local tool settings)
99101
.mcp.json
102+
# next-agents-md
103+
.next-docs/

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!-- NEXT-AGENTS-MD-START -->[Next.js Docs Index]|root: ./.next-docs|STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.|If docs missing, run this command first: npx @next/codemod agents-md --output AGENTS.md|01-app:{04-glossary.mdx}|01-app/01-getting-started:{01-installation.mdx,02-project-structure.mdx,03-layouts-and-pages.mdx,04-linking-and-navigating.mdx,05-server-and-client-components.mdx,06-fetching-data.mdx,07-mutating-data.mdx,08-caching.mdx,09-revalidating.mdx,10-error-handling.mdx,11-css.mdx,12-images.mdx,13-fonts.mdx,14-metadata-and-og-images.mdx,15-route-handlers.mdx,16-proxy.mdx,17-deploying.mdx,18-upgrading.mdx}|01-app/02-guides:{ai-agents.mdx,analytics.mdx,authentication.mdx,backend-for-frontend.mdx,caching-without-cache-components.mdx,ci-build-caching.mdx,content-security-policy.mdx,css-in-js.mdx,custom-server.mdx,data-security.mdx,debugging.mdx,draft-mode.mdx,environment-variables.mdx,forms.mdx,incremental-static-regeneration.mdx,instant-navigation.mdx,instrumentation.mdx,internationalization.mdx,json-ld.mdx,lazy-loading.mdx,local-development.mdx,mcp.mdx,mdx.mdx,memory-usage.mdx,migrating-to-cache-components.mdx,multi-tenant.mdx,multi-zones.mdx,open-telemetry.mdx,package-bundling.mdx,prefetching.mdx,preserving-ui-state.mdx,production-checklist.mdx,progressive-web-apps.mdx,public-static-pages.mdx,redirecting.mdx,sass.mdx,scripts.mdx,self-hosting.mdx,single-page-applications.mdx,static-exports.mdx,streaming.mdx,tailwind-v3-css.mdx,third-party-libraries.mdx,videos.mdx}|01-app/02-guides/migrating:{app-router-migration.mdx,from-create-react-app.mdx,from-vite.mdx}|01-app/02-guides/testing:{cypress.mdx,jest.mdx,playwright.mdx,vitest.mdx}|01-app/02-guides/upgrading:{codemods.mdx,version-14.mdx,version-15.mdx,version-16.mdx}|01-app/03-api-reference:{07-edge.mdx,08-turbopack.mdx}|01-app/03-api-reference/01-directives:{use-cache-private.mdx,use-cache-remote.mdx,use-cache.mdx,use-client.mdx,use-server.mdx}|01-app/03-api-reference/02-components:{font.mdx,form.mdx,image.mdx,link.mdx,script.mdx}|01-app/03-api-reference/03-file-conventions/01-metadata:{app-icons.mdx,manifest.mdx,opengraph-image.mdx,robots.mdx,sitemap.mdx}|01-app/03-api-reference/03-file-conventions/02-route-segment-config:{dynamicParams.mdx,instant.mdx,maxDuration.mdx,preferredRegion.mdx,runtime.mdx}|01-app/03-api-reference/03-file-conventions:{default.mdx,dynamic-routes.mdx,error.mdx,forbidden.mdx,instrumentation-client.mdx,instrumentation.mdx,intercepting-routes.mdx,layout.mdx,loading.mdx,mdx-components.mdx,not-found.mdx,page.mdx,parallel-routes.mdx,proxy.mdx,public-folder.mdx,route-groups.mdx,route.mdx,src-folder.mdx,template.mdx,unauthorized.mdx}|01-app/03-api-reference/04-functions:{after.mdx,cacheLife.mdx,cacheTag.mdx,catchError.mdx,connection.mdx,cookies.mdx,draft-mode.mdx,fetch.mdx,forbidden.mdx,generate-image-metadata.mdx,generate-metadata.mdx,generate-sitemaps.mdx,generate-static-params.mdx,generate-viewport.mdx,headers.mdx,image-response.mdx,next-request.mdx,next-response.mdx,not-found.mdx,permanentRedirect.mdx,redirect.mdx,refresh.mdx,revalidatePath.mdx,revalidateTag.mdx,unauthorized.mdx,unstable_cache.mdx,unstable_noStore.mdx,unstable_rethrow.mdx,updateTag.mdx,use-link-status.mdx,use-params.mdx,use-pathname.mdx,use-report-web-vitals.mdx,use-router.mdx,use-search-params.mdx,use-selected-layout-segment.mdx,use-selected-layout-segments.mdx,userAgent.mdx}|01-app/03-api-reference/05-config/01-next-config-js:{adapterPath.mdx,allowedDevOrigins.mdx,appDir.mdx,assetPrefix.mdx,authInterrupts.mdx,basePath.mdx,cacheComponents.mdx,cacheHandlers.mdx,cacheLife.mdx,compress.mdx,crossOrigin.mdx,cssChunking.mdx,deploymentId.mdx,devIndicators.mdx,distDir.mdx,env.mdx,expireTime.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,htmlLimitedBots.mdx,httpAgentOptions.mdx,images.mdx,incrementalCacheHandlerPath.mdx,inlineCss.mdx,logging.mdx,mdxRs.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,productionBrowserSourceMaps.mdx,proxyClientMaxBodySize.mdx,reactCompiler.mdx,reactMaxHeadersLength.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,sassOptions.mdx,serverActions.mdx,serverComponentsHmrCache.mdx,serverExternalPackages.mdx,staleTimes.mdx,staticGeneration.mdx,taint.mdx,trailingSlash.mdx,transpilePackages.mdx,turbopack.mdx,turbopackFileSystemCache.mdx,turbopackIgnoreIssue.mdx,typedRoutes.mdx,typescript.mdx,urlImports.mdx,useLightningcss.mdx,viewTransition.mdx,webVitalsAttribution.mdx,webpack.mdx}|01-app/03-api-reference/05-config:{02-typescript.mdx,03-eslint.mdx}|01-app/03-api-reference/06-cli:{create-next-app.mdx,next.mdx}|02-pages/01-getting-started:{01-installation.mdx,02-project-structure.mdx,04-images.mdx,05-fonts.mdx,06-css.mdx,11-deploying.mdx}|02-pages/02-guides:{analytics.mdx,authentication.mdx,babel.mdx,ci-build-caching.mdx,content-security-policy.mdx,css-in-js.mdx,custom-server.mdx,debugging.mdx,draft-mode.mdx,environment-variables.mdx,forms.mdx,incremental-static-regeneration.mdx,instrumentation.mdx,internationalization.mdx,lazy-loading.mdx,mdx.mdx,multi-zones.mdx,open-telemetry.mdx,package-bundling.mdx,post-css.mdx,preview-mode.mdx,production-checklist.mdx,redirecting.mdx,sass.mdx,scripts.mdx,self-hosting.mdx,static-exports.mdx,tailwind-v3-css.mdx,third-party-libraries.mdx}|02-pages/02-guides/migrating:{app-router-migration.mdx,from-create-react-app.mdx,from-vite.mdx}|02-pages/02-guides/testing:{cypress.mdx,jest.mdx,playwright.mdx,vitest.mdx}|02-pages/02-guides/upgrading:{codemods.mdx,version-10.mdx,version-11.mdx,version-12.mdx,version-13.mdx,version-14.mdx,version-9.mdx}|02-pages/03-building-your-application/01-routing:{01-pages-and-layouts.mdx,02-dynamic-routes.mdx,03-linking-and-navigating.mdx,05-custom-app.mdx,06-custom-document.mdx,07-api-routes.mdx,08-custom-error.mdx}|02-pages/03-building-your-application/02-rendering:{01-server-side-rendering.mdx,02-static-site-generation.mdx,04-automatic-static-optimization.mdx,05-client-side-rendering.mdx}|02-pages/03-building-your-application/03-data-fetching:{01-get-static-props.mdx,02-get-static-paths.mdx,03-forms-and-mutations.mdx,03-get-server-side-props.mdx,05-client-side.mdx}|02-pages/03-building-your-application/06-configuring:{12-error-handling.mdx}|02-pages/04-api-reference:{06-edge.mdx,08-turbopack.mdx}|02-pages/04-api-reference/01-components:{font.mdx,form.mdx,head.mdx,image-legacy.mdx,image.mdx,link.mdx,script.mdx}|02-pages/04-api-reference/02-file-conventions:{instrumentation.mdx,proxy.mdx,public-folder.mdx,src-folder.mdx}|02-pages/04-api-reference/03-functions:{get-initial-props.mdx,get-server-side-props.mdx,get-static-paths.mdx,get-static-props.mdx,next-request.mdx,next-response.mdx,use-params.mdx,use-report-web-vitals.mdx,use-router.mdx,use-search-params.mdx,userAgent.mdx}|02-pages/04-api-reference/04-config/01-next-config-js:{adapterPath.mdx,allowedDevOrigins.mdx,assetPrefix.mdx,basePath.mdx,bundlePagesRouterDependencies.mdx,compress.mdx,crossOrigin.mdx,deploymentId.mdx,devIndicators.mdx,distDir.mdx,env.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,httpAgentOptions.mdx,images.mdx,logging.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,productionBrowserSourceMaps.mdx,proxyClientMaxBodySize.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,serverExternalPackages.mdx,trailingSlash.mdx,transpilePackages.mdx,turbopack.mdx,typescript.mdx,urlImports.mdx,useLightningcss.mdx,webVitalsAttribution.mdx,webpack.mdx}|02-pages/04-api-reference/04-config:{01-typescript.mdx,02-eslint.mdx}|02-pages/04-api-reference/05-cli:{create-next-app.mdx,next.mdx}|03-architecture:{accessibility.mdx,fast-refresh.mdx,nextjs-compiler.mdx,supported-browsers.mdx}|04-community:{01-contribution-guide.mdx,02-rspack.mdx}<!-- NEXT-AGENTS-MD-END -->
2+
3+
Use test:e2e:headless:agent instead of test:e2e:headless to run tests without a never-ending process.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

pages/about.tsx renamed to app/about/page.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import Link from 'next/link';
2-
import Head from 'components/head';
2+
import type { Metadata } from 'next';
33
import HeroBanner from 'components/HeroBanner/HeroBanner';
44
import Content from 'components/Content/Content';
55
import ImageCard from 'components/Cards/ImageCard/ImageCard';
66
import ValueCard from 'components/Cards/ValueCard/ValueCard';
77
import OutboundLink from 'components/OutboundLink/OutboundLink';
88
import { s3 } from 'common/constants/urls';
99

10+
export const metadata: Metadata = { title: 'About Us' };
11+
1012
const pageTitle = 'About Us';
1113

1214
function About() {
1315
return (
1416
<div>
15-
<Head title={pageTitle} />
16-
1717
<HeroBanner
1818
backgroundImageSource={`${s3}redesign/heroBanners/about.jpg`}
1919
className="lg:bg-top bg-position-[center_3rem] min-h-[60dvh]"
@@ -34,7 +34,7 @@ function About() {
3434
<p>
3535
We at Operation Code strive to provide an efficient way into a tech career for
3636
veterans, military spouses, and transitioning servicemembers. Read about our{' '}
37-
<Link href="/history">organization&apos;s history </Link>
37+
<Link href="/history">organization's history </Link>
3838
to learn more!
3939
</p>
4040

@@ -48,8 +48,8 @@ function About() {
4848
<p>
4949
As a non-profit organization, we rely heavily on your support. If you are interested
5050
in helping us financially, please donate here or set your Amazon Smile organization to
51-
&ldquo;Operation Code&rdquo;. If you have questions about our organization, platforms,
52-
or services, please reference our <Link href="/faq">FAQ</Link> page. Otherwise, do not
51+
Operation Code. If you have questions about our organization, platforms, or
52+
services, please reference our <Link href="/faq">FAQ</Link> page. Otherwise, do not
5353
hesitate to reach out to our staff.
5454
</p>
5555
</div>,
@@ -69,8 +69,8 @@ function About() {
6969
>
7070
<h6>Mentorship Program</h6>
7171
<p>
72-
Operation Code&apos;s mentorship program connects members with seasoned software
73-
developers to help you progress and achieve your goals.
72+
Operation Code's mentorship program connects members with seasoned software developers
73+
to help you progress and achieve your goals.
7474
</p>
7575
</ImageCard>,
7676
<ImageCard
@@ -81,8 +81,8 @@ function About() {
8181
>
8282
<h6>Online Scholarships</h6>
8383
<p>
84-
Operation Code&apos;s online scholarships provide you the opportunity to kickstart
85-
your career in software development.
84+
Operation Code's online scholarships provide you the opportunity to kickstart your
85+
career in software development.
8686
</p>
8787
</ImageCard>,
8888
<ImageCard
@@ -93,7 +93,7 @@ function About() {
9393
>
9494
<h6>Career Services</h6>
9595
<p>
96-
Operation Code&apos;s career services team provides job opportunities, resume reviews,
96+
Operation Code's career services team provides job opportunities, resume reviews,
9797
technical interview prep, and career guidance.
9898
</p>
9999
</ImageCard>,
@@ -138,7 +138,7 @@ function About() {
138138
<h6>Podcast</h6>
139139
<p>
140140
<Link href="/podcast">We have a podcast!</Link> You can listen into the amazing
141-
stories of our members. Visualize your success through others&apos; footsteps.
141+
stories of our members. Visualize your success through others' footsteps.
142142
</p>
143143
</ImageCard>,
144144
]}
@@ -152,7 +152,7 @@ function About() {
152152
<p key="mission">
153153
Operation Code is leading the way to expand opportunities for military veterans and
154154
their families. We aim to help veterans learn new skills and build their careers in the
155-
fast-growing technology sector. Our team&apos;s mission - led by veterans and other
155+
fast-growing technology sector. Our team's mission - led by veterans and other
156156
dedicated, passionate volunteers - is to help open doors for our diverse member base
157157
through unique program offerings, such as our Software Mentor Program, conference
158158
scholarships, and employment services. All of this is made possible by individual

app/api/registration/new/route.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse, type NextRequest } from 'next/server';
2+
import Airtable from 'airtable';
3+
import { AIR_TABLE_BASE_ID, AIR_TABLE_TABLE_NAME } from 'common/config/environment';
4+
import type { RegistrationFormValues } from 'components/Forms/RegistrationForm/RegistrationForm';
5+
6+
const base = new Airtable({ apiKey: process.env.AIRTABLE_PAT }).base(AIR_TABLE_BASE_ID);
7+
8+
export async function POST(request: NextRequest) {
9+
const { email, firstName, lastName, zipcode } = (await request.json()) as RegistrationFormValues;
10+
11+
try {
12+
const records = await base(AIR_TABLE_TABLE_NAME)
13+
.select({ filterByFormula: `{Email} = '${email}'` })
14+
.firstPage();
15+
16+
if (records.length > 0) {
17+
return NextResponse.json(
18+
{ message: `This email has already been registered with an application.` },
19+
{ status: 409 },
20+
);
21+
}
22+
23+
await base(AIR_TABLE_TABLE_NAME).create({
24+
Name: lastName ? `${firstName} ${lastName}` : firstName,
25+
Email: email,
26+
Zipcode: zipcode,
27+
Date: new Date().toISOString(),
28+
});
29+
30+
const response = NextResponse.json({ message: 'Success' });
31+
response.cookies.set('opCodeApplicantEmail', email, { path: '/', httpOnly: true });
32+
return response;
33+
} catch (error) {
34+
console.error('Error with /api/registration/new POST request:', error);
35+
return NextResponse.json(
36+
{ message: `Unexpected Error: Please contact us via staff@operationcode.org` },
37+
{ status: 500 },
38+
);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
1-
import type { NextApiRequest, NextApiResponse } from 'next';
1+
import { NextResponse, type NextRequest } from 'next/server';
22
import Airtable from 'airtable';
33
import { AIR_TABLE_BASE_ID, AIR_TABLE_TABLE_NAME } from 'common/config/environment';
44
import type { UpdateProfileFormShape } from 'components/Forms/UpdateProfileForm/UpdateProfileForm';
55

66
const base = new Airtable({ apiKey: process.env.AIRTABLE_PAT }).base(AIR_TABLE_BASE_ID);
77

8-
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9-
if (req.method !== 'PATCH') {
10-
return res.status(405).json({ message: 'Method Not Allowed' });
11-
}
12-
13-
const email = req.cookies?.opCodeApplicantEmail;
8+
export async function PATCH(request: NextRequest) {
9+
const email = request.cookies.get('opCodeApplicantEmail')?.value;
1410

15-
// The cookie is cleared when the client sends `finalize: true` on the final submit.
16-
// Additional PATCH requests can still arrive after that (e.g. user double-clicking),
17-
// so we need to bail out early rather than querying Airtable with an undefined email.
1811
if (!email) {
19-
return res.status(401).json({ message: 'Missing registration cookie' });
12+
return NextResponse.json({ message: 'Missing registration cookie' }, { status: 401 });
2013
}
2114

2215
try {
23-
// Search for a record with the relevant email
2416
const records = await base(AIR_TABLE_TABLE_NAME)
2517
.select({ filterByFormula: `{Email} = '${email}'` })
2618
.firstPage();
2719

28-
// Record found, return initial values
2920
if (records.length > 0) {
3021
const relevantRecord = records[0];
3122

@@ -40,7 +31,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
4031
gender,
4132
ethnicity: selectedEthnicityOptions,
4233
educationLevel,
43-
} = req.body as Partial<UpdateProfileFormShape>;
34+
finalize: shouldFinalize,
35+
} = (await request.json()) as Partial<UpdateProfileFormShape> & { finalize?: boolean };
4436

4537
const branchOfService = selectedBranchOfServiceOptions?.map(option => option.value) ?? [];
4638
const ethnicity = selectedEthnicityOptions?.map(option => option.value) ?? [];
@@ -66,46 +58,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
6658
'Education Level': educationLevel,
6759
};
6860

69-
/**
70-
* Since we call this endpoint as the user progresses through the
71-
* form, we may not have all the fields defined. AirTable can then
72-
* throw an error because value would not match the expected type
73-
* for many fields.
74-
*/
7561
const parsedPayload = Object.fromEntries(
7662
Object.entries(payload).filter(([, value]) => {
77-
// No undefined keys
7863
if (!value) return false;
79-
80-
// No empty arrays
8164
if (Array.isArray(value) && value.length === 0) return false;
82-
83-
// No empty strings
8465
if (value === '') return false;
85-
8666
return true;
8767
}),
8868
);
8969

90-
if (req.body.finalize) {
91-
res.setHeader('Set-Cookie', [
92-
`opCodeApplicantEmail=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
93-
]);
70+
const response = NextResponse.json({ message: 'Success' });
71+
72+
if (shouldFinalize) {
73+
response.cookies.set('opCodeApplicantEmail', '', {
74+
path: '/',
75+
expires: new Date(0),
76+
});
9477
}
9578

96-
// Update the record with the new values
9779
await base(AIR_TABLE_TABLE_NAME).update(relevantRecord.id, parsedPayload);
9880

99-
return res.status(200).json({ message: 'Success' });
81+
return response;
10082
} else {
101-
// Clear the stale cookie so the /join/form page guard redirects to /join
102-
res.setHeader('Set-Cookie', [
103-
`opCodeApplicantEmail=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
104-
]);
105-
return res.status(404).json({ message: `No record found for this email (${email})` });
83+
const response = NextResponse.json(
84+
{ message: `No record found for this email (${email})` },
85+
{ status: 404 },
86+
);
87+
response.cookies.set('opCodeApplicantEmail', '', {
88+
path: '/',
89+
expires: new Date(0),
90+
});
91+
return response;
10692
}
10793
} catch (error) {
10894
console.error('Error with /api/registration/update PATCH request:', error);
109-
return res.status(500).json({ message: 'Server Error' });
95+
return NextResponse.json({ message: 'Server Error' }, { status: 500 });
11096
}
11197
}

app/blog/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Metadata } from 'next';
2+
import HeroBanner from 'components/HeroBanner/HeroBanner';
3+
4+
export const metadata: Metadata = { title: 'Blog' };
5+
6+
const pageTitle = 'Blog';
7+
8+
export default function BlogIndex() {
9+
return <HeroBanner title={pageTitle} className="min-h-[35dvh]" />;
10+
}

0 commit comments

Comments
 (0)