Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
intiial draft
Browse files Browse the repository at this point in the history
  • Loading branch information
btabaska committed Sep 10, 2024
1 parent de661aa commit 7d44941
Show file tree
Hide file tree
Showing 20 changed files with 10,622 additions and 16,790 deletions.
26,472 changes: 9,730 additions & 16,742 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@opentelemetry/api": "^1.8.0",
"@trussworks/react-uswds": "^7.0.0",
"@uswds/uswds": "^3.6.0",
"isomorphic-dompurify": "^2.15.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"next": "^14.2.3",
Expand Down
75 changes: 34 additions & 41 deletions frontend/src/app/[locale]/opportunity/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import {
ApiResponse,
Summary,
Opportunity,
} from "../../../../types/opportunity/opportunityResponseTypes";

import BetaAlert from "src/components/BetaAlert";
import Breadcrumbs from "src/components/Breadcrumbs";
import { GridContainer } from "@trussworks/react-uswds";
import { Metadata } from "next";
import NotFound from "../../../not-found";
import { OPPORTUNITY_CRUMBS } from "src/constants/breadcrumbs";
import OpportunityAwardInfo from "src/components/opportunity/OpportunityAwardInfo";
import OpportunityDescription from "src/components/opportunity/OpportunityDescription";
import OpportunityHistory from "src/components/opportunity/OpportunityHistory";
import OpportunityIntro from "src/components/opportunity/OpportunityIntro";
import OpportunityLink from "src/components/opportunity/OpportunityLink";
import OpportunityListingAPI from "../../../api/OpportunityListingAPI";
import OpportunityStatusWidget from "src/components/opportunity/OpportunityStatusWidget";
import { getTranslations } from "next-intl/server";
import { isSummary } from "../../../../utils/opportunity/isSummary";

export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
Expand All @@ -24,7 +33,6 @@ export default async function OpportunityListing({
params: { id: string };
}) {
const id = Number(params.id);

// Opportunity id needs to be a number greater than 1
if (isNaN(id) || id < 0) {
return <NotFound />;
Expand All @@ -43,47 +51,32 @@ export default async function OpportunityListing({
return <NotFound />;
}

const renderSummary = (summary: Summary) => {
return (
<>
{Object.entries(summary).map(([summaryKey, summaryValue]) => (
<tr key={summaryKey}>
<td className="word-wrap">{`summary.${summaryKey}`}</td>
<td className="word-wrap">{JSON.stringify(summaryValue)}</td>
</tr>
))}
</>
);
};
const opportunityData: Opportunity = opportunity.data;

OPPORTUNITY_CRUMBS.push({
title: opportunityData.opportunity_title,
path: `/opportunity/${opportunityData.opportunity_id}/`,
});

return (
<div className="grid-container">
<div className="grid-row margin-y-4">
<div className="usa-table-container">
<table className="usa-table usa-table--borderless margin-x-auto width-full maxw-desktop-lg">
<thead>
<tr>
<th>Field Name</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{Object.entries(opportunity.data).map(([key, value]) => {
if (key === "summary" && isSummary(value)) {
return renderSummary(value);
} else {
return (
<tr key={key}>
<td className="word-wrap">{key}</td>
<td className="word-wrap">{JSON.stringify(value)}</td>
</tr>
);
}
})}
</tbody>
</table>
<div>
<BetaAlert />
<Breadcrumbs breadcrumbList={OPPORTUNITY_CRUMBS} />
<OpportunityIntro opportunityData={opportunityData} />
<GridContainer>
<div className="grid-row">
<div className="desktop:grid-col-8 tablet:grid-col-12 tablet:order-1 desktop:order-first">
<OpportunityDescription opportunityData={opportunityData} />
<OpportunityLink opportunityData={opportunityData} />
</div>

<div className="desktop:grid-col-4 tablet:grid-col-12 tablet:order-0">
<OpportunityStatusWidget opportunityData={opportunityData} />
<OpportunityAwardInfo opportunityData={opportunityData} />
<OpportunityHistory opportunityData={opportunityData} />
</div>
</div>
</div>
</GridContainer>
</div>
);
}
5 changes: 3 additions & 2 deletions frontend/src/app/[locale]/process/ProcessIntro.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Grid } from "@trussworks/react-uswds";
import { useTranslations, useMessages } from "next-intl";
import { useMessages, useTranslations } from "next-intl";

import ContentLayout from "src/components/ContentLayout";
import { Grid } from "@trussworks/react-uswds";

const ProcessIntro = () => {
const t = useTranslations("Process");
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/app/[locale]/process/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { PROCESS_CRUMBS } from "src/constants/breadcrumbs";
import { getTranslations, unstable_setRequestLocale } from "next-intl/server";

import BetaAlert from "src/components/BetaAlert";

import Breadcrumbs from "src/components/Breadcrumbs";
import PageSEO from "src/components/PageSEO";
import { Metadata } from "next";
import { PROCESS_CRUMBS } from "src/constants/breadcrumbs";
import PageSEO from "src/components/PageSEO";
import ProcessIntro from "src/app/[locale]/process/ProcessIntro";
import ProcessInvolved from "src/app/[locale]/process/ProcessInvolved";
import ProcessMilestones from "src/app/[locale]/process/ProcessMilestones";
import { useTranslations } from "next-intl";
import { getTranslations, unstable_setRequestLocale } from "next-intl/server";

export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
Expand Down
217 changes: 217 additions & 0 deletions frontend/src/app/[locale]/subscribe/SubscribeForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"use client";
import { SUBSCRIBE_CONFIRMATION } from "src/constants/breadcrumbs";
import { ExternalRoutes } from "src/constants/routes";

import { useRouter } from "next/navigation";
import { useState } from "react";
import {
Alert,
Button,
ErrorMessage,
FormGroup,
Label,
TextInput,
} from "@trussworks/react-uswds";

import { Data } from "src/pages/api/subscribe";
import { useTranslations } from "next-intl";

export default function SubscribeForm() {
const t = useTranslations("Subscribe");

const router = useRouter();
const email = ExternalRoutes.EMAIL_SIMPLERGRANTSGOV;

const [formSubmitted, setFormSubmitted] = useState(false);

const [formData, setFormData] = useState({
name: "",
LastName: "",
email: "",
hp: "",
});

const [sendyError, setSendyError] = useState("");
const [erroredEmail, setErroredEmail] = useState("");

const validateField = (fieldName: string) => {
// returns the string "valid" or the i18n key for the error message
const emailRegex =
/^(\D)+(\w)*((\.(\w)+)?)+@(\D)+(\w)*((\.(\D)+(\w)*)+)?(\.)[a-z]{2,}$/g;
if (fieldName === "name" && formData.name === "")
return t("errors.missing_name");
if (fieldName === "email" && formData.email === "")
return t("errors.missing_email");
if (fieldName === "email" && !emailRegex.test(formData.email))
return t("errors.invalid_email");
return "valid";
};

const showError = (fieldName: string): boolean =>
formSubmitted && validateField(fieldName) !== "valid";

const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const fieldName = e.target.name;
const fieldValue = e.target.value;

setFormData((prevState) => ({
...prevState,
[fieldName]: fieldValue,
}));
};

const submitForm = async () => {
const formURL = "api/subscribe";
if (validateField("email") !== "valid" || validateField("name") !== "valid")
return;

const res = await fetch(formURL, {
method: "POST",
body: JSON.stringify(formData),
headers: {
Accept: "application/json",
},
});

if (res.ok) {
const { message } = (await res.json()) as Data;
router.push(`${SUBSCRIBE_CONFIRMATION.path}?sendy=${message as string}`);
return setSendyError("");
} else {
const { error } = (await res.json()) as Data;
console.error("client error", error);
setErroredEmail(formData.email);
return setSendyError(error || "");
}
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setFormSubmitted(true);
submitForm().catch((err) => {
console.error("catch block", err);
});
};

return (
<form data-testid="sendy-form" onSubmit={handleSubmit} noValidate>
{sendyError ? (
<Alert
type="error"
heading={
sendyError === "Already subscribed."
? "You’re already signed up!"
: "An error occurred"
}
headingLevel="h3"
>
{t.rich(
sendyError === "Already subscribed."
? "errors.already_subscribed"
: "errors.sendy",
{
email: (chunks) => (
<a
href={`mailto:${email}`}
target="_blank"
rel="noopener noreferrer"
>
{chunks}
</a>
),
sendy_error: (chunks) => (
<a
href={`mailto:${sendyError}`}
target="_blank"
rel="noopener noreferrer"
>
{chunks}
</a>
),
email_address: (chunks) => (
<a
href={`mailto:${erroredEmail}`}
target="_blank"
rel="noopener noreferrer"
>
{chunks}
</a>
),
},
)}
</Alert>
) : (
<></>
)}
<FormGroup error={showError("name")}>
<Label htmlFor="name">
First Name{" "}
<span title="required" className="usa-hint usa-hint--required ">
(required)
</span>
</Label>
{showError("name") ? (
<ErrorMessage className="maxw-mobile-lg">
{validateField("name")}
</ErrorMessage>
) : (
<></>
)}
<TextInput
aria-required
type="text"
name="name"
id="name"
value={formData.name}
onChange={handleInput}
/>
</FormGroup>
<Label htmlFor="LastName" hint=" (optional)">
Last Name
</Label>
<TextInput
type="text"
name="LastName"
id="LastName"
value={formData.LastName}
onChange={handleInput}
/>
<FormGroup error={showError("email")}>
<Label htmlFor="email">
Email{" "}
<span title="required" className="usa-hint usa-hint--required ">
(required)
</span>
</Label>
{showError("email") ? (
<ErrorMessage className="maxw-mobile-lg">
{validateField("email")}
</ErrorMessage>
) : (
<></>
)}
<TextInput
aria-required
type="email"
name="email"
id="email"
value={formData.email}
onChange={handleInput}
/>
</FormGroup>
<div className="display-none">
<Label htmlFor="hp">HP</Label>
<TextInput
type="text"
name="hp"
id="hp"
value={formData.hp}
onChange={handleInput}
/>
</div>
<Button type="submit" name="submit" id="submit" className="margin-top-4">
Subscribe
</Button>
</form>
);
}
Loading

0 comments on commit 7d44941

Please sign in to comment.