diff --git a/.eslintrc.js b/.eslintrc.js index d28a32e08..8f59b3990 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', @@ -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', }, diff --git a/.gitignore b/.gitignore index c3f7b497c..081a9cd0f 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,8 @@ out # storybook build output .storybook-dist +.storybook-static +storybook-static # WebStorm Config .idea @@ -97,3 +99,5 @@ tsconfig.tsbuildinfo # MCP config (local tool settings) .mcp.json +# next-agents-md +.next-docs/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..f688de020 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +[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} + +Use test:e2e:headless:agent instead of test:e2e:headless to run tests without a never-ending process. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..43c994c2d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/pages/about.tsx b/app/about/page.tsx similarity index 88% rename from pages/about.tsx rename to app/about/page.tsx index cb90667b4..a73168041 100644 --- a/pages/about.tsx +++ b/app/about/page.tsx @@ -1,5 +1,5 @@ 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'; @@ -7,13 +7,13 @@ 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 (
- - We at Operation Code strive to provide an efficient way into a tech career for veterans, military spouses, and transitioning servicemembers. Read about our{' '} - organization's history + organization's history to learn more!

@@ -48,8 +48,8 @@ function About() {

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 - “Operation Code”. If you have questions about our organization, platforms, - or services, please reference our FAQ page. Otherwise, do not + “Operation Code”. If you have questions about our organization, platforms, or + services, please reference our FAQ page. Otherwise, do not hesitate to reach out to our staff.

, @@ -69,8 +69,8 @@ function About() { >
Mentorship Program

- Operation Code'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.

,
Online Scholarships

- Operation Code'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.

,
Career Services

- Operation Code'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.

, @@ -138,7 +138,7 @@ function About() {
Podcast

We have a podcast! You can listen into the amazing - stories of our members. Visualize your success through others' footsteps. + stories of our members. Visualize your success through others' footsteps.

, ]} @@ -152,7 +152,7 @@ function About() {

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'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 diff --git a/app/api/registration/new/route.ts b/app/api/registration/new/route.ts new file mode 100644 index 000000000..dce91cbd1 --- /dev/null +++ b/app/api/registration/new/route.ts @@ -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 }, + ); + } +} diff --git a/pages/api/registration/update.ts b/app/api/registration/update/route.ts similarity index 56% rename from pages/api/registration/update.ts rename to app/api/registration/update/route.ts index aff0eff97..7073b1077 100644 --- a/pages/api/registration/update.ts +++ b/app/api/registration/update/route.ts @@ -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]; @@ -40,7 +31,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) gender, ethnicity: selectedEthnicityOptions, educationLevel, - } = req.body as Partial; + finalize: shouldFinalize, + } = (await request.json()) as Partial & { finalize?: boolean }; const branchOfService = selectedBranchOfServiceOptions?.map(option => option.value) ?? []; const ethnicity = selectedEthnicityOptions?.map(option => option.value) ?? []; @@ -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 }); } } diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 000000000..a4da39534 --- /dev/null +++ b/app/blog/page.tsx @@ -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 ; +} diff --git a/pages/branding.tsx b/app/branding/page.tsx similarity index 91% rename from pages/branding.tsx rename to app/branding/page.tsx index 85d405595..d8d87dea4 100644 --- a/pages/branding.tsx +++ b/app/branding/page.tsx @@ -1,4 +1,4 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import LogoSection from 'components/Branding/LogoSection/LogoSection'; import ColorSection from 'components/Branding/ColorSection/ColorSection'; @@ -7,13 +7,13 @@ import OutboundLink from 'components/OutboundLink/OutboundLink'; import { s3 } from 'common/constants/urls'; import LinkButton from 'components/Buttons/LinkButton/LinkButton'; +export const metadata: Metadata = { title: 'Branding Guide' }; + const pageTitle = 'Branding Guide'; function Branding() { return ( <> - -

@@ -45,7 +45,7 @@ function Branding() {

Please note: Images may appear larger or smaller than they appear on your device, but the files - linked are "large" or "small" as described. + linked are “large” or “small” as described.

@@ -55,7 +55,7 @@ function Branding() { analyticsEventLabel="Branding Storybook Link" href="https://storybook.operationcode.org/?path=/story/heading--default" > - Operation Code's Storybook Component Library here. + Operation Code's Storybook Component Library here.

diff --git a/pages/challenge.tsx b/app/challenge/page.tsx similarity index 72% rename from pages/challenge.tsx rename to app/challenge/page.tsx index 056f45a26..681b07190 100644 --- a/pages/challenge.tsx +++ b/app/challenge/page.tsx @@ -1,5 +1,5 @@ +import type { Metadata } from 'next'; import { s3 } from 'common/constants/urls'; -import Head from 'components/head'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Content from 'components/Content/Content'; import OutboundLink from 'components/OutboundLink/OutboundLink'; @@ -7,6 +7,8 @@ import challengers from 'static/operationcode_challenge/names'; import range from 'lodash/range'; import Image from 'next/image'; +export const metadata: Metadata = { title: 'Challenge' }; + const pageTitle = 'Challenge'; const RepoLink = 'https://github.com/OperationCode/front-end/'; @@ -42,14 +44,12 @@ export const NamesColumns = () => { function Challenge() { return (
- -

Welcome to the Operation Code challenge! The goal of this challenge is to get you to easily commit your first change to a program, see the results of the change, and leave - your mark on Operation Code itself! To do this we're going to take a look at a source - code repository, clone the repository, make a change to a file and finally create a pull + your mark on Operation Code itself! To do this we're going to take a look at a source code + repository, clone the repository, make a change to a file and finally create a pull request.

@@ -88,16 +88,16 @@ function Challenge() { width={65} height={65} />{' '} - In a few moments, you will be redirected to your own copy of this website's - source code. + In a few moments, you will be redirected to your own copy of this website's source + code.

- Congratulations! You have "forked" the "repo"! + Congratulations! You have “forked” the “repo”!
  • - Now that you have a fork of the "repo", it's time to edit the - necessary file to add your name to the list below! Go to the /public + Now that you have a fork of the “repo”, it's time to edit the necessary file to add + your name to the list below! Go to the /public {` folder`}, then the /static {` folder`}, click on the operationcode_challenge directory and click on the file called names.js. On the right-hand side, you should see @@ -114,11 +114,11 @@ function Challenge() {
  • Scroll to the bottom for the Commit changes form. There are two input boxes. - In the input field with "Update names.js", type{' '} + In the input field with “Update names.js”, type{' '} Add <YOUR NAME> to challenge list. You will leave the second, - large input field blank. There are two "radio" buttons below the input - fields. Check the one that says "Create a new branch". Your screen should - now have something like this: + large input field blank. There are two “radio” buttons below the input fields. Check + the one that says “Create a new branch”. Your screen should now have something like + this:

    - Operation Code's front-end repository + Operation Code's front-end repository .
  • - Click on the "Pull requests" tab. It rests between the "Issues" - and "Project" tabs. + Click on the “Pull requests” tab. It rests between the “Issues” and “Project” tabs.
  • -
  • Click on the green "New pull request" button.
  • +
  • Click on the green “New pull request” button.
  • - You should now be at the " - Open a pull request - " screen. We do not wish to ask ourselves for permission to merge our new - branch into our own fork! Instead,  click{' '} + You should now be at the “Open a pull request“ screen. We do not wish to ask + ourselves for permission to merge our new branch into our own fork! Instead,  + click{' '} {' '} -  , to open Operation Code's "New pull request" interface. You - should see a "Compare changes" headline. Just below that is a link within - the text:  'compare across forks' - click it. Now, click on the - selector that says ' head fork' at the beginning, and choose your fork. - Click the next selector to the right, and choose your new branch. Now, you're - comparing Operation Code's main branch with your new fork's branch, and - you may click +  , to open Operation Code's “New pull request” interface. You should see a + “Compare changes” headline. Just below that is a link within the text:  + 'compare across forks' - click it. Now, click on the selector that says ' head fork' + at the beginning, and choose your fork. Click the next selector to the right, and + choose your new branch. Now, you're comparing Operation Code's main branch with your + new fork's branch, and you may click screenshot of the 'Create pull request' buttonNOTE: {' '} A pull request is how people throughout the world are able to contribute to open - source software - like Operation Code's website! When you submit a pull request - it notifies the maintainers of the project, and runs some automated checks. The + source software - like Operation Code's website! When you submit a pull request it + notifies the maintainers of the project, and runs some automated checks. The maintainers then look at the new changes, and decide if they want it merged into their repository.
  • - When you're ready, click the "Create pull request" button. Our staff - will be notified and a few minutes after the pull request is accepted and merged - your name will show up below! + When you're ready, click the “Create pull request” button. Our staff will be + notified and a few minutes after the pull request is accepted and merged your name + will show up below!
  • - Congratulations - you've made your first open source commit! + Congratulations - you've made your first open source commit!
    , ]} diff --git a/pages/chapter_leader.tsx b/app/chapter_leader/page.tsx similarity index 97% rename from pages/chapter_leader.tsx rename to app/chapter_leader/page.tsx index dd3d6a4b6..348d60b31 100644 --- a/pages/chapter_leader.tsx +++ b/app/chapter_leader/page.tsx @@ -1,15 +1,15 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Content from 'components/Content/Content'; import OutboundLink from 'components/OutboundLink/OutboundLink'; +export const metadata: Metadata = { title: 'Chapter Leaders' }; + const pageTitle = 'Chapter Leaders'; function ChapterLeader() { return (
    - -

    Operation Code is looking for volunteer Chapter Leaders to build local communities diff --git a/pages/chapters.tsx b/app/chapters/page.tsx similarity index 93% rename from pages/chapters.tsx rename to app/chapters/page.tsx index 77d513b98..cb58fe694 100644 --- a/pages/chapters.tsx +++ b/app/chapters/page.tsx @@ -1,9 +1,11 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Content from 'components/Content/Content'; import FlatCard from 'components/Cards/FlatCard/FlatCard'; import OutboundLink from 'components/OutboundLink/OutboundLink'; +export const metadata: Metadata = { title: 'Chapters' }; + const pageTitle = 'Chapters'; const unsortedChapterLocations = [ @@ -36,8 +38,6 @@ const chapterLocations = unsortedChapterLocations.sort(({ name: nameA }, { name: function Chapters() { return ( <> - -

    Get involved by joing your local chapter!

    @@ -62,7 +62,7 @@ function Chapters() { })}
    ,
    - Don't see your a location in your area? + Don't see your a location in your area?
    - - - Department of Labor, Women's Bureau + Department of Labor, Women's Bureau ] @@ -109,7 +109,6 @@ const biases: Bias[] = [ const CorporateTraining = () => { return ( <> - { Operation Code has provided corporate training for tech employers since 2019. In order to break barriers and blockers for our military community, we must address the implicit and overt biases. Reach out to us if you would like more information on how our military - cultural competency training works, if you'd like us to help create a military - Diversity, Equity, Inclusion and Belonging strategy, provide ongoing professional - development or set up a military Employee Resource Group with you: Contact the{' '} + cultural competency training works, if you'd like us to help create a military Diversity, + Equity, Inclusion and Belonging strategy, provide ongoing professional development or set + up a military Employee Resource Group with you: Contact the{' '} { key={bias.title} className={cx( 'flex md:even:flex-row-reverse md:flex-row flex-col-reverse flex-wrap md:flex-nowrap md:[&>*]:flex-1', - 'even:bg-secondary even:text-white', // mobile alternating colors per li - 'md:[&:nth-child(1n)]:bg-white md:[&:nth-child(2n)]:bg-theme-gray-800 md:[&:nth-child(3n)]:bg-secondary md:[&:nth-child(1n)]:text-secondary md:[&:nth-child(3n)]:text-white', // non-mobile alternating colors per li + 'even:bg-secondary even:text-white', + 'md:[&:nth-child(1n)]:bg-white md:[&:nth-child(2n)]:bg-theme-gray-800 md:[&:nth-child(3n)]:bg-secondary md:[&:nth-child(1n)]:text-secondary md:[&:nth-child(3n)]:text-white', )} >
    @@ -156,7 +155,7 @@ const CorporateTraining = () => {
    diff --git a/pages/donate.tsx b/app/donate/page.tsx similarity index 96% rename from pages/donate.tsx rename to app/donate/page.tsx index 5c1b38155..4526dd77d 100644 --- a/pages/donate.tsx +++ b/app/donate/page.tsx @@ -1,15 +1,15 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import Container from 'components/Container/Container'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import OutboundLink from 'components/OutboundLink/OutboundLink'; const pageTitle = 'Donate'; +export const metadata: Metadata = { title: pageTitle }; + function DonatePage() { return ( <> - - @@ -29,7 +29,7 @@ function DonatePage() { Your donations also helps our community reduce the risk facing our transitioning military, military spouses and military veterans by growing social connectedness, building camaraderie and teaching tangible technical and personal skills that combat - chronic unemployment, homelessness, and suicide. You're providing members with the + chronic unemployment, homelessness, and suicide. You're providing members with the opportunity to learn software development, enter the tech industry, and Deploy The Future!

    diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 000000000..83954b6b5 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useEffect } from 'react'; +import * as Sentry from '@sentry/nextjs'; +import ErrorDisplay from 'components/ErrorDisplay/ErrorDisplay'; + +export default function Error({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ; +} diff --git a/pages/faq.tsx b/app/faq/page.tsx similarity index 81% rename from pages/faq.tsx rename to app/faq/page.tsx index e965f9c30..1e8cafc44 100644 --- a/pages/faq.tsx +++ b/app/faq/page.tsx @@ -1,10 +1,12 @@ 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 Accordion from 'components/Accordion/Accordion'; import OutboundLink from 'components/OutboundLink/OutboundLink'; +export const metadata: Metadata = { title: 'FAQ' }; + const questions = { general: [ { @@ -42,11 +44,11 @@ const questions = { content: ( <> Operation Code, much like software, is built from anywhere with an internet connection, - and is not based in one location. While we're headquartered in Portland, the entire + and is not based in one location. While we're headquartered in Portland, the entire organization is decentralized, including the board of directors and the core team. This - allows us to more effectively serve the entire military community, whether they're - veterans or military spouses, whether they're OCONUS or in-country. We have chapters - all over the nation. Use Slack chat and join the closest town to you! + allows us to more effectively serve the entire military community, whether they're + veterans or military spouses, whether they're OCONUS or in-country. We have chapters all + over the nation. Use Slack chat and join the closest town to you! ), }, @@ -54,10 +56,10 @@ const questions = { title: 'Who does Operation Code serve?', content: ( <> - Operation Code serves our nation's finest who've worn the uniform and their - families who are interested in coding and software development. Our programs are offered - at no cost to the military community, including veterans, transitioning service members, - and military spouses and families. + Operation Code serves our nation's finest who've worn the uniform and their families who + are interested in coding and software development. Our programs are offered at no cost to + the military community, including veterans, transitioning service members, and military + spouses and families. ), }, @@ -105,7 +107,7 @@ const questions = { <> While we do not have a long-term mentorship program, mentors are available for 30-minute sessions to assist you with things like mock interviews, code reviews, or general - guidance. To request a mentorship session, type "/mentor" in any of our{' '} + guidance. To request a mentorship session, type “/mentor” in any of our{' '} Slack channels {' '} @@ -117,8 +119,8 @@ const questions = { title: 'What are the hours of operation for Operation Code?', content: ( <> - Operation Code is different in that we don't have regular business office hours. The - team can usually be found in, our{' '} + Operation Code is different in that we don't have regular business office hours. The team + can usually be found in, our{' '} Slack channel {' '} @@ -160,8 +162,8 @@ const questions = { Facebook {' '} - to put out updates and news since it's faster to put out info and respond. Given our - chosen craft, we don't do regular emails as often. + to put out updates and news since it's faster to put out info and respond. Given our + chosen craft, we don't do regular emails as often. ), }, @@ -169,14 +171,14 @@ const questions = { title: "My question isn't listed. How do I contact Operation Code?", content: ( <> - If you have a question that isn't listed here on our FAQ, write to{' '} + If you have a question that isn't listed here on our FAQ, write to{' '} staff@operationcode.org {' '} - , and we'll get back to you as soon as we can. + , and we'll get back to you as soon as we can. ), }, @@ -195,8 +197,8 @@ const questions = { title: 'I would rather mail a check. To whom do I make it out and where do I send it?', content: ( <> - It's less administrative work to accept online donations. Get in touch so we can - assess your situation and contribution commitment. + It's less administrative work to accept online donations. Get in touch so we can assess + your situation and contribution commitment. ), }, @@ -225,7 +227,7 @@ const questions = { directly benefit the organization, transitioning military, citizen-soldiers, veterans and their families in learning to code and building software to change the world. Items that are needed, include (but not limited to): frequent flyer miles, Adobe Cloud, used or new - MacBook Air's, and grant writers. + MacBook Air's, and grant writers. ), }, @@ -234,9 +236,8 @@ const questions = { do that?`, content: ( <> - Get in touch, and we'll make an announcement in our Slack, tweet and/or write a blog - post, and find a veteran to take your spot. Even then, travel and lodging is often a - barrier. + Get in touch, and we'll make an announcement in our Slack, tweet and/or write a blog post, + and find a veteran to take your spot. Even then, travel and lodging is often a barrier. ), }, @@ -266,14 +267,13 @@ const questions = { https://smile.amazon.com {' '} , you continue to have the same shopping experience as the same and most products - available on amazon.com but you help Operation Code realize it 's mission. Once - you’ve selected "Operation Code" everything else functions the same. Shop for - your favorite products or the perfect gift. Most products are eligible on Amazon Smile, if - not, you’ll be notified. You can checkout normally as well. No extra cost is passed onto - you–Amazon will donate 0.5% of your purchase to Operation Code! After you’ve successfully - completed a purchase on AmazonSmile you can share the news with your friends on Facebook, - Twitter or via email. This option appears on the confirmation page after your order is - complete. + available on amazon.com but you help Operation Code realize it 's mission. Once you've + selected “Operation Code” everything else functions the same. Shop for your favorite + products or the perfect gift. Most products are eligible on Amazon Smile, if not, you'll + be notified. You can checkout normally as well. No extra cost is passed onto you–Amazon + will donate 0.5% of your purchase to Operation Code! After you've successfully completed a + purchase on AmazonSmile you can share the news with your friends on Facebook, Twitter or + via email. This option appears on the confirmation page after your order is complete. ), }, @@ -303,8 +303,6 @@ const questions = { function FAQ() { return (
    - - , @@ -49,8 +51,6 @@ const supportItems = [ function GetInvolved() { return (
    - -

    - We're always looking for volunteers who are dedicated to making an impact in the - lives of military veterans, service members, and spouses. + We're always looking for volunteers who are dedicated to making an impact in the lives + of military veterans, service members, and spouses.

    You can help us with:

    , diff --git a/app/global-error.tsx b/app/global-error.tsx new file mode 100644 index 000000000..61a644911 --- /dev/null +++ b/app/global-error.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { useEffect } from 'react'; +import * as Sentry from '@sentry/nextjs'; +import ErrorDisplay from 'components/ErrorDisplay/ErrorDisplay'; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/pages/history.tsx b/app/history/page.tsx similarity index 80% rename from pages/history.tsx rename to app/history/page.tsx index 68c3a8caa..dc3c6c9d7 100644 --- a/pages/history.tsx +++ b/app/history/page.tsx @@ -1,16 +1,17 @@ +import type { Metadata } from 'next'; import { s3 } from 'common/constants/urls'; import Content from 'components/Content/Content'; -import Head from 'components/head'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Timeline from 'components/Timeline/Timeline'; import TimelineNav from 'components/Timeline/TimelineNav/TimelineNav'; const pageTitle = 'History'; +export const metadata: Metadata = { title: pageTitle }; + function History() { return (
    -
    - “There are no secrets to success. It is the result of preparation, hard work, - learning from failure.” + “There are no secrets to success. It is the result of preparation, hard work, learning + from failure.”
    - Colin Powell
    diff --git a/pages/jobs.tsx b/app/jobs/page.tsx similarity index 90% rename from pages/jobs.tsx rename to app/jobs/page.tsx index 0fa4695d3..a464598e6 100644 --- a/pages/jobs.tsx +++ b/app/jobs/page.tsx @@ -1,4 +1,4 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Content from 'components/Content/Content'; import FeaturedJobsData from 'components/FeaturedJobItem/featuredJobs.json'; @@ -7,11 +7,11 @@ import ZipRecruiterJobs from 'components/ZipRecruiterJobs/ZipRecruiterJobs'; const pageTitle = 'Jobs'; +export const metadata: Metadata = { title: pageTitle }; + function Jobs() { return ( <> - - { - prefetch(profileUpdateURL); - }, []); +export default function JoinContent({ hasRegistrationError }: { hasRegistrationError: boolean }) { + const router = useRouter(); + const [isErrorModalOpen, setIsErrorModalOpen] = useState(hasRegistrationError); useEffect(() => { - if (query.registrationError) { - setIsErrorModalOpen(true); - } - }, [query.registrationError]); + router.prefetch(profileUpdateURL); + }, [router]); const handleSuccess = () => { gtag.conversionEvent({ adId: '9ZvVCOOFmrkBEK-Rnp4D', category: 'sign_up' }); - push(profileUpdateURL); + router.push(profileUpdateURL); }; return ( <> - -

    @@ -65,12 +58,12 @@ export default function Join() {

    Registration Incomplete

    - It looks like we're missing information from the first step of registration. Please + It looks like we're missing information from the first step of registration. Please complete the form below to get started.

    - If you've already completed this step and were unexpectedly redirected here, - something may be wrong on our end. Please email us at{' '} + If you've already completed this step and were unexpectedly redirected here, something + may be wrong on our end. Please email us at{' '} staff@operationcode.org {' '} diff --git a/app/join/form/page.tsx b/app/join/form/page.tsx new file mode 100644 index 000000000..6c9274d0e --- /dev/null +++ b/app/join/form/page.tsx @@ -0,0 +1,27 @@ +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import type { Metadata } from 'next'; +import HeroBanner from 'components/HeroBanner/HeroBanner'; +import Content from 'components/Content/Content'; +import UpdateProfileForm from 'components/Forms/UpdateProfileForm/UpdateProfileForm'; + +export const metadata: Metadata = { title: 'Update Profile' }; + +const pageTitle = 'Update Profile'; + +export default async function UpdateProfile() { + const cookieStore = await cookies(); + const opCodeApplicantEmail = cookieStore.get('opCodeApplicantEmail'); + + if (!opCodeApplicantEmail) { + redirect('/'); + } + + return ( + <> + + + ]} /> + + ); +} diff --git a/app/join/page.tsx b/app/join/page.tsx new file mode 100644 index 000000000..1f1d01ec6 --- /dev/null +++ b/app/join/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next'; +import JoinContent from './JoinContent'; + +export const metadata: Metadata = { title: 'Join' }; + +export default async function JoinPage({ + searchParams, +}: { + searchParams: Promise<{ registrationError?: string }>; +}) { + const { registrationError } = await searchParams; + + return ; +} diff --git a/app/join/success/page.tsx b/app/join/success/page.tsx new file mode 100644 index 000000000..18346c160 --- /dev/null +++ b/app/join/success/page.tsx @@ -0,0 +1,32 @@ +import { SUCCESS_PAGE_MESSAGE } from 'common/constants/testIDs'; +import type { Metadata } from 'next'; +import LinkButton from 'components/Buttons/LinkButton/LinkButton'; +import HeroBanner from 'components/HeroBanner/HeroBanner'; +import OutboundLink from 'components/OutboundLink/OutboundLink'; + +export const metadata: Metadata = { title: 'Successful Registration!' }; + +const pageTitle = `Successful Registration!`; + +export default function JoinSuccess() { + return ( + +

    + We will review your application and send an invite to our Slack team as soon as possible. If + you do not receive an invite within a week, please email us at{' '} + + staff@operationcode.org + + . +

    + + + Go Home + + + ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..548d0f301 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,84 @@ +import 'common/styles/globals.css'; + +import type { Metadata } from 'next'; +import Script from 'next/script'; +import type { PropsWithChildren } from 'react'; + +import { clientTokens } from 'common/config/environment'; +import { AnalyticsProvider } from 'components/Analytics/AnalyticsProvider'; +import Footer from 'components/Footer/Footer'; +import Nav from 'components/Nav/Nav'; +import { ScrollToTopButton } from 'components/ScrollToTopButton/ScrollToTopButton'; + +const defaultOgImage = `https://operation-code-assets.s3.us-east-2.amazonaws.com/branding/oc_image.png`; + +export const metadata: Metadata = { + title: { + template: 'Operation Code | %s', + default: 'Operation Code', + }, + description: + 'Operation Code is a registered 501(c)3 whose mission is to help our military community and SIV allied refugees grow in their tech careers while rebuilding our lives post-conflict.', + icons: { + icon: '/public/favicon.ico', + apple: '/static/apple-icon-180x180.png', + }, + openGraph: { + type: 'website', + url: 'https://operationcode.org', + images: { + width: 1200, + height: 630, + alt: 'Operation Code Logo', + url: defaultOgImage, + }, + }, + twitter: { + card: 'summary_large_image', + site: 'https://operationcode.org', + images: { + alt: 'Operation Code Logo', + url: defaultOgImage, + }, + }, +}; + +export default function RootLayout({ children }: PropsWithChildren) { + const isProduction = process.env.VERCEL_ENV === 'production'; + + return ( + + + + {isProduction ? ( + + ) : null} + + + +
    +
    + + + ); +} diff --git a/pages/404.tsx b/app/not-found.tsx similarity index 74% rename from pages/404.tsx rename to app/not-found.tsx index 858c68640..58ed17079 100644 --- a/pages/404.tsx +++ b/app/not-found.tsx @@ -1,5 +1,5 @@ import ErrorDisplay from 'components/ErrorDisplay/ErrorDisplay'; -export default function Custom404() { +export default function NotFound() { return ; } diff --git a/pages/index.tsx b/app/page.tsx similarity index 82% rename from pages/index.tsx rename to app/page.tsx index 571adbf34..ab66e9245 100644 --- a/pages/index.tsx +++ b/app/page.tsx @@ -1,4 +1,4 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Content from 'components/Content/Content'; import JoinSection from 'components/ReusableSections/JoinSection/JoinSection'; @@ -9,11 +9,13 @@ import LinkButton from 'components/Buttons/LinkButton/LinkButton'; import { s3 } from 'common/constants/urls'; import { cx } from 'common/utils/cva'; +export const metadata: Metadata = { title: 'Home' }; + const successStories = [ { title: 'Ali Cipolla-Taylor, Talent Acquisition at Microsoft', quote: - 'I finished MSTA the last week of February, and then COVID hit. Employment was not going to happen…to anyone. I kept making calls, working on my skills, and throwing myself out there, and I got a role as a vendor at Microsoft. I’m half of the Data Privacy, Compliance, and Controls team for Talent Acquisition now. I had a lot of hard conversations with myself. I learned to lean into a support network, locally and online, through OpCode. I’m notoriously shy on the internet, but I knew that I couldn’t do this alone. Change happens when the discomfort of making the change is less than the life you’re living.', + "I finished MSTA the last week of February, and then COVID hit. Employment was not going to happen…to anyone. I kept making calls, working on my skills, and throwing myself out there, and I got a role as a vendor at Microsoft. I'm half of the Data Privacy, Compliance, and Controls team for Talent Acquisition now. I had a lot of hard conversations with myself. I learned to lean into a support network, locally and online, through OpCode. I'm notoriously shy on the internet, but I knew that I couldn't do this alone. Change happens when the discomfort of making the change is less than the life you're living.", imageSource: `${s3}headshots/ali.jpg`, }, { @@ -33,8 +35,6 @@ const successStories = [ function Home() { return (
    - -

    - We're the largest community of military veterans, service members, and spouses - committed to becoming software developers with the help of mentors, scholarships, and - our tech partners. + We're the largest community of military veterans, service members, and spouses committed + to becoming software developers with the help of mentors, scholarships, and our tech + partners.

    diff --git a/app/podcast/PodcastPlayer.tsx b/app/podcast/PodcastPlayer.tsx new file mode 100644 index 000000000..3e594c2f6 --- /dev/null +++ b/app/podcast/PodcastPlayer.tsx @@ -0,0 +1,27 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +const ReactPlayer = dynamic(() => import('react-player/lazy'), { ssr: false }); + +interface PodcastPlayerProps { + url: string; +} + +export default function PodcastPlayer({ url }: PodcastPlayerProps) { + return ( + + ); +} diff --git a/pages/podcast.tsx b/app/podcast/page.tsx similarity index 61% rename from pages/podcast.tsx rename to app/podcast/page.tsx index 2030c1df7..9a2ead3bc 100644 --- a/pages/podcast.tsx +++ b/app/podcast/page.tsx @@ -1,14 +1,17 @@ import axios from 'axios'; import get from 'lodash/get'; -import dynamic from 'next/dynamic'; import { parse as parseXml } from 'fast-xml-parser'; -import { ONE_DAY } from 'common/constants/unitsOfTime'; -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Card from 'components/Cards/Card/Card'; import Content from 'components/Content/Content'; import Heading from 'components/Heading/Heading'; import Image from 'next/image'; +import PodcastPlayer from './PodcastPlayer'; + +export const metadata: Metadata = { title: 'Podcast' }; + +export const revalidate = 86400; interface RSS { channel: { @@ -23,12 +26,7 @@ interface Episode { story: string; } -const pageTitle = 'Podcast'; - -const ReactPlayer = dynamic(() => import('react-player/lazy'), { ssr: false }); - -// We have atypical error handling because there exist errors thrown on nearly every request. -export async function getStaticProps() { +async function getEpisodes(): Promise { const { data } = await axios.get('https://operationcode.libsyn.com/rss'); const { @@ -44,25 +42,24 @@ export async function getStaticProps() { const numberOfEpisodes = get(rss, 'channel.item.length', 0); if (numberOfEpisodes > 0) { - const episodes = rss.channel.item.map(({ image: { href }, link, title, description }) => ({ + return rss.channel.item.map(({ image: { href }, link, title, description }) => ({ image: href, name: title[0], source: link, story: description.replace(/(

    |<\/p>)/g, ''), })); - - return { props: { episodes }, revalidate: ONE_DAY }; } - // Request failed or RSS Feed is broken... Break the build! - throw new Error('getStaticProps in /podcast failed.'); + throw new Error('Failed to fetch podcast episodes.'); } -function Podcast({ episodes }: { episodes: Episode[] }) { +const pageTitle = 'Podcast'; + +export default async function Podcast() { + const episodes = await getEpisodes(); + return (

    - -

    Come listen to some inspiring stories of our vets transitioning into tech!

    @@ -71,13 +68,6 @@ function Podcast({ episodes }: { episodes: Episode[] }) { columns={[
    {episodes.map(({ name, image, source, story }, index) => { - /* - * Some episodes have multiple parts and are named like "${Name}, part 1". - * Some episodes are named "${Name} Interview" - * - * Parsing them in this manner ensures that the name of the interviewee is - * available and used for the image alt tag. - */ const interviewee = name.replace(/ interview/gi, '').split(',')[0]; return ( @@ -97,19 +87,7 @@ function Podcast({ episodes }: { episodes: Episode[] }) { height={200} /> - +

    {story}

    @@ -121,5 +99,3 @@ function Podcast({ episodes }: { episodes: Episode[] }) {
    ); } - -export default Podcast; diff --git a/pages/policy.tsx b/app/policy/page.tsx similarity index 94% rename from pages/policy.tsx rename to app/policy/page.tsx index fc06b3a57..e794df3ac 100644 --- a/pages/policy.tsx +++ b/app/policy/page.tsx @@ -1,12 +1,12 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import { s3 } from 'common/constants/urls'; +export const metadata: Metadata = { title: 'Policy' }; + function Policy() { return (
    - - - -

    - This page is designed to make a journalist's job easy in writing, blogging, or - documenting Operation Code. Below you will find targeted information corresponding to - common representative visitors, videos, photos, press releases, and branding details. + This page is designed to make a journalist's job easy in writing, blogging, or documenting + Operation Code. Below you will find targeted information corresponding to common + representative visitors, videos, photos, press releases, and branding details.

    @@ -56,11 +56,11 @@ function Press() { We have long-standing, productive partnerships with some amazing companies, and yours could be one of them! Organizations that put our members and our open source work on a pedastal, can look forward to receive social media blasts and the - appreciation of America's military veterans. If you are thinking about a + appreciation of America's military veterans. If you are thinking about a partnership with Operation Code, but are unsure of what to offer our members,{' '} - let's talk. - If you're seeking information to display in announcing the partnership, - please see below! + let's talk. If + you're seeking information to display in announcing the partnership, please see + below!

    @@ -70,10 +70,10 @@ function Press() {

    Media Outlets

    The staff at Operation Code thank you for taking your time to represent us in your - work. If your piece has a specific theme or target, and you'd like some - custom contributions, please join our organization to receive a Slack team invite. - There you'll likely find many Operation Code members willing and able to - offer personal anecdotes and first-hand interviews! + work. If your piece has a specific theme or target, and you'd like some custom + contributions, please join our organization to receive a Slack team invite. There + you'll likely find many Operation Code members willing and able to offer personal + anecdotes and first-hand interviews!

    diff --git a/pages/project_rebuild.tsx b/app/project_rebuild/page.tsx similarity index 91% rename from pages/project_rebuild.tsx rename to app/project_rebuild/page.tsx index 96008cde1..46d746323 100644 --- a/pages/project_rebuild.tsx +++ b/app/project_rebuild/page.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import Head from 'components/head'; +import type { Metadata } from 'next'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import Content from 'components/Content/Content'; import OutboundLink from 'components/OutboundLink/OutboundLink'; @@ -8,18 +8,11 @@ import { s3 } from 'common/constants/urls'; const pageTitle = 'Project Rebuild'; +export const metadata: Metadata = { title: pageTitle }; + function ProjectRebuild() { return (
    - -

    In conjunction with Fresh Start Refugee Assistance Center, an Afghan-American led - non-profit, and Globally.org’s ReUp Refugee Tech Re-Skilling Program, Operation Code + non-profit, and Globally.org's ReUp Refugee Tech Re-Skilling Program, Operation Code is pleased to announce that we are expanding our Project Rebuild Refugee Tech Training Program. Since March 2022, Operation Code launched the initial pilot cohort, with 8 Afghan refugee participants in attendance. We have expanded the cohorts to include @@ -76,7 +69,7 @@ function ProjectRebuild() {

    Fresh Start provides wraparound services such as: - initial refugee resettlement efforts, ESL classes, driver’s education and licensing, + initial refugee resettlement efforts, ESL classes, driver's education and licensing, affordable housing, mental health and cultural transition support as well as job search assistance.

    @@ -88,7 +81,7 @@ function ProjectRebuild() { {' '} by providing our refugee participants a scholarship to complete one certification during the six-month cohort. Pairing a refugee with a Veteran or military spouse - mentor to meet on a regular cadence, our two communities can continue to “rebuild” our + mentor to meet on a regular cadence, our two communities can continue to "rebuild" our parallel experiences, provide a tech-focused workforce development program and help refugees obtain high paid and meaningful work.

    diff --git a/pages/scholarship/code_platoon.tsx b/app/scholarship/code_platoon/CodePlatoonContent.tsx similarity index 90% rename from pages/scholarship/code_platoon.tsx rename to app/scholarship/code_platoon/CodePlatoonContent.tsx index fab141e65..65d93a4e7 100644 --- a/pages/scholarship/code_platoon.tsx +++ b/app/scholarship/code_platoon/CodePlatoonContent.tsx @@ -1,11 +1,12 @@ -import Head from 'components/head'; +'use client'; + +import { useEffect } from 'react'; import Container from 'components/Container/Container'; import HeroBanner from 'components/HeroBanner/HeroBanner'; -import { useEffect } from 'react'; const pageTitle = 'Code Platoon X Operation Code Bootcamp Scholarship'; -export default function CodePlatoonScholarshipPage() { +export default function CodePlatoonContent() { useEffect(() => { const script = document.createElement('script'); script.src = 'https://js.hsforms.net/forms/embed/v2.js'; @@ -33,7 +34,6 @@ export default function CodePlatoonScholarshipPage() { return ( <> -
    diff --git a/app/scholarship/code_platoon/page.tsx b/app/scholarship/code_platoon/page.tsx new file mode 100644 index 000000000..675b689c0 --- /dev/null +++ b/app/scholarship/code_platoon/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; +import CodePlatoonContent from './CodePlatoonContent'; + +export const metadata: Metadata = { + title: 'Code Platoon X Operation Code Bootcamp Scholarship', +}; + +export default function CodePlatoonScholarshipPage() { + return ; +} diff --git a/pages/scholarship/index.tsx b/app/scholarship/page.tsx similarity index 95% rename from pages/scholarship/index.tsx rename to app/scholarship/page.tsx index 869a3a121..7ba1418f3 100644 --- a/pages/scholarship/index.tsx +++ b/app/scholarship/page.tsx @@ -1,4 +1,4 @@ -import Head from 'components/head'; +import type { Metadata } from 'next'; import Container from 'components/Container/Container'; import HeroBanner from 'components/HeroBanner/HeroBanner'; import OutboundLink from 'components/OutboundLink/OutboundLink'; @@ -7,6 +7,8 @@ import Link from 'next/link'; import Card from 'components/Cards/Card/Card'; import Image from 'next/image'; +export const metadata: Metadata = { title: 'Scholarships Program' }; + const pageTitle = 'Scholarships Program'; interface ScholarshipOption { @@ -23,7 +25,7 @@ const scholarshipOptions: ScholarshipOption[] = [ { title: 'Code Platoon X Operation Code', logoSrc: `${s3}partnerLogos/code_platoon.png`, - body: 'Apply to attend a full ride scholarship to one of the Code Platoon’s coding bootcamp cohorts.', + body: "Apply to attend a full ride scholarship to one of the Code Platoon's coding bootcamp cohorts.", link: '/scholarship/code_platoon', }, { @@ -61,8 +63,6 @@ const scholarshipOptions: ScholarshipOption[] = [ export default function ScholarshipsPage() { return ( <> - - , @@ -29,8 +31,6 @@ const mentorItems = [ function Services() { return (
    - -
  • - If you're new to coding, learn some basics at{' '} + If you're new to coding, learn some basics at{' '} Ask people who have attended a coding bootcamp or coding bootcamp recruiters by joining @@ -57,7 +59,7 @@ const questions = { ), }, { - title: `I’m a recent bootcamp/college graduate or looking for a job.`, + title: `I'm a recent bootcamp/college graduate or looking for a job.`, content: (
    • @@ -96,10 +98,10 @@ const questions = { ), }, { - title: `I’m an admissions recruiter or representative from a Coding School`, + title: `I'm an admissions recruiter or representative from a Coding School`, content: ( <> - You’re welcome to post information and answer questions about your Coding School on{' '} + You're welcome to post information and answer questions about your Coding School on{' '} #coding-schools only based on the{' '} - Explore our numerous channels on a specific subject, or if you don’t see a channel ask on + Explore our numerous channels on a specific subject, or if you don't see a channel ask on the #help or create a new channel.{' '} Be advised @@ -181,7 +183,7 @@ const questions = { <> Our instructions are focused on seeing-eye users. If you use assistive technology on - your computer, [Slack's own + your computer, [Slack's own guide](https://slack.com/help/articles/360000411963-Use-Slack-with-a-screen-reader) will be more helpful. @@ -195,8 +197,8 @@ const questions = { />

      - Once you’ve found a channel that interests you, click the green “Join Channel” button. - You’re ready to go! Have fun, learn, and connect with others! + Once you've found a channel that interests you, click the green "Join Channel" button. + You're ready to go! Have fun, learn, and connect with others!

      - -