Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ module.exports = {
'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }],
'react/jsx-filename-extension': ['error', { extensions: ['.js', '.tsx'] }],
'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }],
'react/no-unescaped-entities': 'off',
'react/jsx-no-target-blank': 'off', // browsers protect against this vulnerability now
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'react/jsx-one-expression-per-line': 'off',
Expand Down Expand Up @@ -284,18 +285,13 @@ module.exports = {
},
},
{
files: ['./pages/api/**/*.ts'],
files: ['./app/api/**/*.ts'],
rules: {
'no-console': 'off',
},
},
{
files: [
'pages/**.js',
'components/head.js',
'components/nav.js',
'components/Timeline/historyData.js',
],
files: ['components/nav.js', 'components/Timeline/historyData.js'],
rules: {
'react/react-in-jsx-scope': 'off',
},
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ out

# storybook build output
.storybook-dist
.storybook-static
storybook-static

# WebStorm Config
.idea
Expand All @@ -97,3 +99,5 @@ tsconfig.tsbuildinfo

# MCP config (local tool settings)
.mcp.json
# next-agents-md
.next-docs/
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- 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 -->

Use test:e2e:headless:agent instead of test:e2e:headless to run tests without a never-ending process.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
26 changes: 13 additions & 13 deletions pages/about.tsx → app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import Link from 'next/link';
import Head from 'components/head';
import type { Metadata } from 'next';
import HeroBanner from 'components/HeroBanner/HeroBanner';
import Content from 'components/Content/Content';
import ImageCard from 'components/Cards/ImageCard/ImageCard';
import ValueCard from 'components/Cards/ValueCard/ValueCard';
import OutboundLink from 'components/OutboundLink/OutboundLink';
import { s3 } from 'common/constants/urls';

export const metadata: Metadata = { title: 'About Us' };

const pageTitle = 'About Us';

function About() {
return (
<div>
<Head title={pageTitle} />

<HeroBanner
backgroundImageSource={`${s3}redesign/heroBanners/about.jpg`}
className="lg:bg-top bg-position-[center_3rem] min-h-[60dvh]"
Expand All @@ -34,7 +34,7 @@ function About() {
<p>
We at Operation Code strive to provide an efficient way into a tech career for
veterans, military spouses, and transitioning servicemembers. Read about our{' '}
<Link href="/history">organization&apos;s history </Link>
<Link href="/history">organization's history </Link>
to learn more!
</p>

Expand All @@ -48,8 +48,8 @@ function About() {
<p>
As a non-profit organization, we rely heavily on your support. If you are interested
in helping us financially, please donate here or set your Amazon Smile organization to
&ldquo;Operation Code&rdquo;. If you have questions about our organization, platforms,
or services, please reference our <Link href="/faq">FAQ</Link> page. Otherwise, do not
Operation Code. If you have questions about our organization, platforms, or
services, please reference our <Link href="/faq">FAQ</Link> page. Otherwise, do not
hesitate to reach out to our staff.
</p>
</div>,
Expand All @@ -69,8 +69,8 @@ function About() {
>
<h6>Mentorship Program</h6>
<p>
Operation Code&apos;s mentorship program connects members with seasoned software
developers to help you progress and achieve your goals.
Operation Code's mentorship program connects members with seasoned software developers
to help you progress and achieve your goals.
</p>
</ImageCard>,
<ImageCard
Expand All @@ -81,8 +81,8 @@ function About() {
>
<h6>Online Scholarships</h6>
<p>
Operation Code&apos;s online scholarships provide you the opportunity to kickstart
your career in software development.
Operation Code's online scholarships provide you the opportunity to kickstart your
career in software development.
</p>
</ImageCard>,
<ImageCard
Expand All @@ -93,7 +93,7 @@ function About() {
>
<h6>Career Services</h6>
<p>
Operation Code&apos;s career services team provides job opportunities, resume reviews,
Operation Code's career services team provides job opportunities, resume reviews,
technical interview prep, and career guidance.
</p>
</ImageCard>,
Expand Down Expand Up @@ -138,7 +138,7 @@ function About() {
<h6>Podcast</h6>
<p>
<Link href="/podcast">We have a podcast!</Link> You can listen into the amazing
stories of our members. Visualize your success through others&apos; footsteps.
stories of our members. Visualize your success through others' footsteps.
</p>
</ImageCard>,
]}
Expand All @@ -152,7 +152,7 @@ function About() {
<p key="mission">
Operation Code is leading the way to expand opportunities for military veterans and
their families. We aim to help veterans learn new skills and build their careers in the
fast-growing technology sector. Our team&apos;s mission - led by veterans and other
fast-growing technology sector. Our team's mission - led by veterans and other
dedicated, passionate volunteers - is to help open doors for our diverse member base
through unique program offerings, such as our Software Mentor Program, conference
scholarships, and employment services. All of this is made possible by individual
Expand Down
40 changes: 40 additions & 0 deletions app/api/registration/new/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextResponse, type NextRequest } from 'next/server';
import Airtable from 'airtable';
import { AIR_TABLE_BASE_ID, AIR_TABLE_TABLE_NAME } from 'common/config/environment';
import type { RegistrationFormValues } from 'components/Forms/RegistrationForm/RegistrationForm';

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

export async function POST(request: NextRequest) {
const { email, firstName, lastName, zipcode } = (await request.json()) as RegistrationFormValues;

try {
const records = await base(AIR_TABLE_TABLE_NAME)
.select({ filterByFormula: `{Email} = '${email}'` })
.firstPage();

if (records.length > 0) {
return NextResponse.json(
{ message: `This email has already been registered with an application.` },
{ status: 409 },
);
}

await base(AIR_TABLE_TABLE_NAME).create({
Name: lastName ? `${firstName} ${lastName}` : firstName,
Email: email,
Zipcode: zipcode,
Date: new Date().toISOString(),
});

const response = NextResponse.json({ message: 'Success' });
response.cookies.set('opCodeApplicantEmail', email, { path: '/', httpOnly: true });
return response;
} catch (error) {
console.error('Error with /api/registration/new POST request:', error);
return NextResponse.json(
{ message: `Unexpected Error: Please contact us via staff@operationcode.org` },
{ status: 500 },
);
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextResponse, type NextRequest } from 'next/server';
import Airtable from 'airtable';
import { AIR_TABLE_BASE_ID, AIR_TABLE_TABLE_NAME } from 'common/config/environment';
import type { UpdateProfileFormShape } from 'components/Forms/UpdateProfileForm/UpdateProfileForm';

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

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'PATCH') {
return res.status(405).json({ message: 'Method Not Allowed' });
}

const email = req.cookies?.opCodeApplicantEmail;
export async function PATCH(request: NextRequest) {
const email = request.cookies.get('opCodeApplicantEmail')?.value;

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

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

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

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

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

/**
* Since we call this endpoint as the user progresses through the
* form, we may not have all the fields defined. AirTable can then
* throw an error because value would not match the expected type
* for many fields.
*/
const parsedPayload = Object.fromEntries(
Object.entries(payload).filter(([, value]) => {
// No undefined keys
if (!value) return false;

// No empty arrays
if (Array.isArray(value) && value.length === 0) return false;

// No empty strings
if (value === '') return false;

return true;
}),
);

if (req.body.finalize) {
res.setHeader('Set-Cookie', [
`opCodeApplicantEmail=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
]);
const response = NextResponse.json({ message: 'Success' });

if (shouldFinalize) {
response.cookies.set('opCodeApplicantEmail', '', {
path: '/',
expires: new Date(0),
});
}

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

return res.status(200).json({ message: 'Success' });
return response;
} else {
// Clear the stale cookie so the /join/form page guard redirects to /join
res.setHeader('Set-Cookie', [
`opCodeApplicantEmail=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
]);
return res.status(404).json({ message: `No record found for this email (${email})` });
const response = NextResponse.json(
{ message: `No record found for this email (${email})` },
{ status: 404 },
);
response.cookies.set('opCodeApplicantEmail', '', {
path: '/',
expires: new Date(0),
});
return response;
}
} catch (error) {
console.error('Error with /api/registration/update PATCH request:', error);
return res.status(500).json({ message: 'Server Error' });
return NextResponse.json({ message: 'Server Error' }, { status: 500 });
}
}
10 changes: 10 additions & 0 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Metadata } from 'next';
import HeroBanner from 'components/HeroBanner/HeroBanner';

export const metadata: Metadata = { title: 'Blog' };

const pageTitle = 'Blog';

export default function BlogIndex() {
return <HeroBanner title={pageTitle} className="min-h-[35dvh]" />;
}
Loading
Loading