Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/my applications #1079

Merged
merged 10 commits into from
Apr 9, 2021
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: 10 additions & 0 deletions sites/public/cypress/integration/pages/MyApplications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
describe("My applications page", function () {
it("renders the my applications page", function () {
cy.visit("/account/applications")
cy.getByID("email").type("admin@example.com")
cy.getByID("password").type("abcdef")
cy.get("button").contains("Sign In").click()
cy.visit("/account/applications")
cy.contains("My Applications")
})
})
30 changes: 30 additions & 0 deletions sites/public/pages/account/AppStatusItemWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useContext, useEffect, useState } from "react"
import { Application, Listing } from "@bloom-housing/backend-core/types"
import { AppStatusItem, ApiClientContext } from "@bloom-housing/ui-components"

interface AppStatusItemWrapperProps {
application: Application
}

const AppStatusItemWrapper = (props: AppStatusItemWrapperProps) => {
const { listingsService } = useContext(ApiClientContext)
const [listing, setListing] = useState<Listing>()

useEffect(() => {
listingsService
?.retrieve({ listingId: props.application.listing.id })
.then((retrievedListing) => {
setListing(retrievedListing)
})
.catch((err) => console.error(`Error fetching listing: ${err}`))
}, [listingsService, props.application])

return listing ? (
<AppStatusItem application={props.application} listing={listing} key={props.application.id} />
) : (
// Potential for a loading state here
<></>
)
}

export { AppStatusItemWrapper as default, AppStatusItemWrapper }
124 changes: 124 additions & 0 deletions sites/public/pages/account/application.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useEffect, useState, useContext } from "react"
import {
ApiClientContext,
RequireLogin,
t,
UserContext,
FormCard,
dateToString,
} from "@bloom-housing/ui-components"
import Link from "next/link"
import FormSummaryDetails from "../../src/forms/applications/FormSummaryDetails"
import FormsLayout from "../../layouts/forms"
import { Application, Listing } from "@bloom-housing/backend-core/types"
import { useRouter } from "next/router"

export default () => {
const router = useRouter()
const applicationId = router.query.id as string
const { applicationsService, listingsService } = useContext(ApiClientContext)
const { profile } = useContext(UserContext)
const [application, setApplication] = useState<Application>()
const [listing, setListing] = useState<Listing>()
const [unauthorized, setUnauthorized] = useState(false)
const [noApplication, setNoApplication] = useState(false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 403/404 state hooks are neat. I wonder if we could make a standard hook that would provide this and set the appropriate state on either 404 or 403, so we could use it on other pages. This might be good to review as well if we move towards using SWR everywhere: https://swr.vercel.app/docs/error-handling

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored this to be a little cleaner, but would hope we could accomplish this in another ticket?

useEffect(() => {
if (profile) {
applicationsService
.retrieve({ applicationId })
.then((app) => {
setApplication(app)
listingsService
?.retrieve({ listingId: app.listing.id })
.then((retrievedListing) => {
setListing(retrievedListing)
})
.catch((err) => {
console.error(`Error fetching listing: ${err}`)
})
})
.catch((err) => {
console.error(`Error fetching application: ${err}`)
const { status } = err.response || {}
if (status === 404) {
setNoApplication(true)
}
if (status === 403) {
setUnauthorized(true)
}
})
}
}, [profile, applicationId, applicationsService, listingsService])

return (
<>
<RequireLogin signInPath="/sign-in" signInMessage={t("t.loginIsRequired")}>
<FormsLayout>
{noApplication && (
<FormCard header={t("account.application.error")}>
<p className="field-note mb-5">{t("account.application.noApplicationError")}</p>
<a href={`applications`} className="button is-small">
{t("account.application.return")}
</a>
</FormCard>
)}
{unauthorized && (
<FormCard header={t("account.application.error")}>
<p className="field-note mb-5">{t("account.application.noAccessError")}</p>
<a href={`applications`} className="button is-small">
{t("account.application.return")}
</a>
</FormCard>
)}
{application && (
<>
<FormCard header={t("account.application.confirmation")}>
<div className="py-2">
{listing && (
<Link
href={`listing/id=${listing.id}`}
as={`${origin}/listing/${listing.id}/${listing.urlSlug}`}
>
<a className="lined text-tiny">
{t("application.confirmation.viewOriginalListing")}
</a>
</Link>
)}
</div>
</FormCard>

<FormCard>
<div className="form-card__lead border-b">
<h2 className="form-card__title is-borderless">
{t("application.confirmation.informationSubmittedTitle")}
</h2>
<p className="field-note mt-4 text-center">
{t("application.confirmation.submitted")}
{dateToString(application.submissionDate)}
</p>
</div>
<div className="form-card__group text-center">
<h3 className="form-card__paragraph-title">
{t("application.confirmation.lotteryNumber")}
</h3>

<p className="font-serif text-3xl my-0">{application.id}</p>
</div>

<FormSummaryDetails application={application} />

<div className="form-card__pager hide-for-print">
<div className="form-card__pager-row py-6">
<a href="#" className="lined text-tiny" onClick={() => window.print()}>
{t("application.confirmation.printCopy")}
</a>
</div>
</div>
</FormCard>
</>
)}
</FormsLayout>
</RequireLogin>
</>
)
}
111 changes: 40 additions & 71 deletions sites/public/pages/account/applications.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,56 @@
import React, { useEffect, useState, Fragment } from "react"
import React, { useEffect, useState, Fragment, useContext } from "react"
import Head from "next/head"
import {
AppearanceBorderType,
AppearanceStyleType,
AppStatusItem,
Button,
ApiClientContext,
DashBlock,
DashBlocks,
HeaderBadge,
LinkButton,
MetaTags,
Modal,
RequireLogin,
t,
UserContext,
} from "@bloom-housing/ui-components"
import Layout from "../../layouts/application"
import moment from "moment"
import { Application, ArcherListing } from "@bloom-housing/backend-core/types"
import { PaginatedApplication } from "@bloom-housing/backend-core/types"
import { AppStatusItemWrapper } from "./AppStatusItemWrapper"

export default () => {
const [applications, setApplications] = useState([])
const [deletingApplication, setDeletingApplication] = useState(null)
const listing = Object.assign({}, ArcherListing)
const { applicationsService } = useContext(ApiClientContext)
const { profile } = useContext(UserContext)
const [applications, setApplications] = useState<PaginatedApplication>()
const [error, setError] = useState(null)

useEffect(() => {
// applicationsService.list().then((apps) => {
// setApplications(apps)
// })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listing = {} as any
const application = {} as Application
listing.applicationDueDate = moment().add(10, "days").format()
application.listing = listing
// TODO: Fix the types here (and probably this shouldn't come from the frontend anyway)
// application.updatedAt = moment().toDate()
setApplications([application])
}, [])
if (profile) {
applicationsService
.list({ userId: profile.id })
.then((apps) => {
setApplications(apps)
})
.catch((err) => {
console.error(`Error fetching applications: ${err}`)
setError(`${err}`)
})
}
}, [profile, applicationsService])

const noApplicationsSection = () => {
return error ? (
<div className="p-8">
<h2 className="pb-4">{`${t("account.errorFetchingApplications")}`}</h2>
</div>
) : (
<div className="p-8">
<h2 className="pb-4">{t("account.noApplications")}</h2>
<LinkButton href="/listings">{t("listings.browseListings")}</LinkButton>
</div>
)
}

const noApplicationsSection = (
<div className="p-8">
<h2 className="pb-4">It looks like you haven't applied to any listings yet.</h2>
<LinkButton href="/listings">{t("listings.browseListings")}</LinkButton>
</div>
)
const modalActions = [
<Button
styleType={AppearanceStyleType.primary}
onClick={() => {
// applicationsService.delete(deletingApplication.id).then(() => {
const newApplications = [...applications]
const deletedAppIndex = applications.indexOf(deletingApplication, 0)
delete newApplications[deletedAppIndex]
setDeletingApplication(null)
setApplications(newApplications)
// })
}}
>
{t("t.delete")}
</Button>,
<Button
styleType={AppearanceStyleType.secondary}
border={AppearanceBorderType.borderless}
onClick={() => {
setDeletingApplication(null)
}}
>
{t("t.cancel")}
</Button>,
]
return (
<>
<RequireLogin signInPath="/sign-in" signInMessage={t("t.loginIsRequired")}>
<Modal
open={deletingApplication}
title={t("application.deleteThisApplication")}
ariaDescription={t("application.deleteThisApplication")}
actions={modalActions}
hideCloseIcon
/>
<Layout>
<Head>
<title>{t("nav.myApplications")}</title>
Expand All @@ -88,17 +61,13 @@ export default () => {
<DashBlocks>
<DashBlock title={t("account.myApplications")} icon={<HeaderBadge />}>
<Fragment>
{applications.map((application) => (
<AppStatusItem
key={application.id}
status="inProgress"
listing={listing}
application={application}
setDeletingApplication={setDeletingApplication}
/>
))}
{applications.length == 0 && noApplicationsSection}
{applications &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably use optionals here to condense it a bit?

applications.items.length > 0 &&
applications.items.map((application, index) => (
<AppStatusItemWrapper key={index} application={application} />
))}
</Fragment>
{!applications && noApplicationsSection()}
</DashBlock>
</DashBlocks>
</div>
Expand Down
Loading