diff --git a/.eslintrc.js b/.eslintrc.js index 5deb32bde..be7e26ef5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,7 @@ module.exports = { "no-underscore-dangle": "off", "react-hooks/exhaustive-deps": "error", // Default is 'warn'; we upgrade to 'error' because otherwise warnings are just noise "react/forbid-prop-types": "off", + "react/no-danger": "off", "react/prefer-stateless-function": "off", "react/prop-types": "off", "react/jsx-filename-extension": ["error", { extensions: [".tsx"] }], @@ -69,7 +70,10 @@ module.exports = { "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { ignoreRestSiblings: true }, + ], "@typescript-eslint/no-unused-expressions": "error", "@typescript-eslint/require-await": "off", "@typescript-eslint/restrict-template-expressions": "off", diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 000000000..4ac0b6107 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,48 @@ +name: Build, lint and run unit tests. + +on: + pull_request: + branches: [main, development] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_VERSION }} + - uses: actions/cache@v2 + with: + path: ~/.npm + key: npm-${{ hashFiles('package-lock.json') }} + restore-keys: npm- + - run: npm i + - run: npm run prettier + - run: npm run lint + build_and_test_app: + runs-on: ubuntu-latest + environment: "dev" + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_VERSION }} + - uses: actions/cache@v2 + with: + path: ~/.npm + key: npm-${{ hashFiles('package-lock.json') }} + restore-keys: npm- + - run: npm i + - run: npm run test + - run: npm run build + env: + AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }} + AUTH0_REDIRECT_URI: ${{ secrets.AUTH0_REDIRECT_URI }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + ALGOLIA_INDEX_PREFIX: ${{ secrets.ALGOLIA_INDEX_PREFIX }} + ALGOLIA_APPLICATION_ID: ${{ secrets.ALGOLIA_APPLICATION_ID }} + ALGOLIA_READ_ONLY_API_KEY: ${{ secrets.ALGOLIA_READ_ONLY_API_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9654091b0..6e82f6c37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions +# Exygy - This is a workflow from Sheltertech that we don't use in the fork name: testsuite diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml new file mode 100644 index 000000000..223b86136 --- /dev/null +++ b/.github/workflows/deploy-development.yml @@ -0,0 +1,45 @@ +name: Build and deploy to heroku staging. + +on: + push: + branches: [development] + +jobs: + build_and_deploy_app: + runs-on: ubuntu-latest + environment: "dev" + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_VERSION }} + - uses: actions/cache@v2 + with: + path: ~/.npm + key: npm-${{ hashFiles('package-lock.json') }} + restore-keys: npm- + - run: npm i + - run: npm run test + - run: npm run build + env: + AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }} + AUTH0_REDIRECT_URI: ${{ secrets.AUTH0_REDIRECT_URI }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + ALGOLIA_INDEX_PREFIX: ${{ secrets.ALGOLIA_INDEX_PREFIX }} + ALGOLIA_APPLICATION_ID: ${{ secrets.ALGOLIA_APPLICATION_ID }} + ALGOLIA_READ_ONLY_API_KEY: ${{ secrets.ALGOLIA_READ_ONLY_API_KEY }} + STRAPI_API_TOKEN: ${{ secrets.STRAPI_API_TOKEN }} + STRAPI_API_URL: ${{ secrets.STRAPI_API_URL }} + - name: Build, Push and Release a Docker container to Heroku. + uses: gonuit/heroku-docker-deploy@v1.3.3 + with: + email: ${{ secrets.HEROKU_EMAIL }} + heroku_api_key: ${{ secrets.HEROKU_API_KEY }} + heroku_app_name: ${{ secrets.HEROKU_APP_NAME }} + dockerfile_directory: ./ + dockerfile_name: Dockerfile + docker_options: "--no-cache" + process_type: web diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 000000000..0a45ce277 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,43 @@ +name: Build and deploy to heroku production. + +on: + push: + branches: [main] + +jobs: + build_and_deploy_app: + runs-on: ubuntu-latest + environment: "prod" + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_VERSION }} + - uses: actions/cache@v2 + with: + path: ~/.npm + key: npm-${{ hashFiles('package-lock.json') }} + restore-keys: npm- + - run: npm i + - run: npm run test + - run: npm run build + env: + AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }} + AUTH0_REDIRECT_URI: ${{ secrets.AUTH0_REDIRECT_URI }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + ALGOLIA_INDEX_PREFIX: ${{ secrets.ALGOLIA_INDEX_PREFIX }} + ALGOLIA_APPLICATION_ID: ${{ secrets.ALGOLIA_APPLICATION_ID }} + ALGOLIA_READ_ONLY_API_KEY: ${{ secrets.ALGOLIA_READ_ONLY_API_KEY }} + - name: Build, Push and Release a Docker container to Heroku. + uses: gonuit/heroku-docker-deploy@v1.3.3 + with: + email: ${{ secrets.HEROKU_EMAIL }} + heroku_api_key: ${{ secrets.HEROKU_API_KEY }} + heroku_app_name: ${{ secrets.HEROKU_APP_NAME }} + dockerfile_directory: ./ + dockerfile_name: Dockerfile + docker_options: "--no-cache" + process_type: web diff --git a/.github/workflows/publish-qa.yml b/.github/workflows/publish-qa.yml index ccfd770e0..45c253e58 100644 --- a/.github/workflows/publish-qa.yml +++ b/.github/workflows/publish-qa.yml @@ -1,3 +1,4 @@ +# Exygy - This is a workflow from Sheltertech that we don't use in the fork name: publish-qa on: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 35a36b310..dd49f6012 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,3 +1,4 @@ +# Exygy - This is a workflow from Sheltertech that we don't use in the fork name: publish-release on: diff --git a/.gitignore b/.gitignore index c8783acef..6c14e38ac 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,16 @@ yarn.lock cypress/videos/** cypress/screenshots/** + +.vscode/* +# !.vscode/settings.json +# !.vscode/tasks.json +# !.vscode/launch.json +# !.vscode/extensions.json +# !.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix diff --git a/Dockerfile b/Dockerfile index 8019e44a0..9dd9c16c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,12 @@ FROM nginx:stable -RUN apt-get update && apt-get -y install ruby && gem install tiller +WORKDIR /app/askdarcel -COPY build /app/askdarcel -COPY version.json /app/askdarcel/_version.json -COPY tools/replace-environment-config.sh /app/askdarcel +# Copy build files +COPY ./build /app/askdarcel RUN rm /etc/nginx/conf.d/* -ADD docker/tiller /etc/tiller +COPY ./docker/templates/default.conf.template /etc/nginx/templates/default.conf.template -CMD ["tiller", "-v"] -ENTRYPOINT ["/app/askdarcel/replace-environment-config.sh", "/app/askdarcel/bundle.js"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 14a245fa3..2d4ffb4b7 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,5 @@ # ShelterTech Web App [![Travis CI Status](https://travis-ci.org/ShelterTechSF/askdarcel-web.svg?branch=master)](https://travis-ci.org/ShelterTechSF/askdarcel-web) -## Sauce Labs Browser Test Status - -[![Sauce Test Status](https://saucelabs.com/browser-matrix/askdarcel-web-master.svg)](https://saucelabs.com/u/askdarcel-web-master) - -## Onboarding Instructions - -[Dev Role Description](https://www.notion.so/sheltertech/Developer-Engineer-Role-Description-ShelterTech-AskDarcel-SFServiceGuide-Tech-Team-7fd992a20f864698a43e3882a66338bb) - -[Technical Onboarding & Team Guidelines](https://www.notion.so/sheltertech/Technical-Onboarding-and-Team-Guidelines-a06d5543495248bfb6f17e233330249e) - ## Docker-based Development Environment (Recommended) ### Requirements @@ -155,3 +145,19 @@ $ docker-compose run --rm -p 1337:1337 -e BASE_URL=http://web:8080 web npm run t ``` This will spin up a web server at http://localhost:1337/ and print out a URL to use. You should manually enter it into your browser to start the tests. + +## Branches and Deployments + +There are two protected branches - development and main. Main is the default branch which will be the latest, stable codebase. Development will be where updates get deployed to a staging instance where QA can be performed. Any PR's created against these branches run a series of checks - like building the app, running unit tests, and linting the files. + +There are two live instances - a [staging instance](https://our415-staging-a91cdc6d7b2b.herokuapp.com/) and a [production instance](https://our415-abb7eecb7449.herokuapp.com/). Merges onto the development branch deploys the development branch to the staging isntance. Merges onto the main branch deploys the main branch to the production instance. See the [github workflows](https://github.com/Exygy/askdarcel-web/tree/main/.github/workflows) for the details. Environment variables used in the deployments are set in github environments - 'prod' supplies the production instance and 'dev' supplies the staging instance.. + +## Pull Requests + +Pull requests are opened to the development branch. When opening a pull request please fill out the as much of the pull request template you can, which includes tagging the issue your PR is related to, a description of your PR, indicating the type of change, including details for the reviewer about how to test your PR, and a testing checklist. Additionally, officially link the notion ticket to the PR using GitHub's linking UI. + +When your PR is ready for review, add the needs review(s) label to help surface it to the other devs. You can assign people as reviewers to surface the work further. If you put up a PR that is not yet ready for eyes, add the wip label. + +Once the PR has been approved, you either (1) squash and merge the commits if your changes are just in one package, or (2) rebase and merge your commits if your commits are cleanly separated across multiple packages to allow the versions to propagate appropriately. + +As a reviewer on a PR, try not to leave only comments, but a clear next step action. If the PR requires further discussion or changes, mark it with Requested Changes. If a PR looks good to you (or even if there are small changes requested that won't require an additional review), please mark it with Approved and comment on the last few changes needed. This helps other reviewers better understand the state of PRs at the list view and prevents an additional unnecessary review cycle. diff --git a/app/App.module.scss b/app/App.module.scss index 77288b189..9e6d02a8d 100644 --- a/app/App.module.scss +++ b/app/App.module.scss @@ -1,3 +1,5 @@ +@import "~styles/utils/_helpers.scss"; + .outerContainer { /* Creates a stacking context such that everything inside of the * .outerContainer is either above or below anything outside of the diff --git a/app/App.tsx b/app/App.tsx index 97cb39738..c57230a31 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -1,45 +1,23 @@ import React, { useEffect, useState } from "react"; - // Todo: Once GA sunsets the UA analytics tracking come July 2023, we can remove the "react-ga" // package and all references to it: // https://support.google.com/analytics/answer/12938611#zippy=%2Cin-this-article -import ReactGA from "react-ga"; import ReactGA_4 from "react-ga4"; - import Intercom from "react-intercom"; import { Helmet } from "react-helmet-async"; import { useHistory } from "react-router-dom"; - import { GeoCoordinates, getLocation, whiteLabel, AppProvider } from "./utils"; -import { - Banner, - HamburgerMenu, - Navigation, - PopUpMessage, - PopupMessageProp, - UserWay, -} from "./components/ui"; - -import { Router } from "./Router"; +import { Navigation, UserWay } from "./components/ui"; import config from "./config"; import MetaImage from "./assets/img/sfsg-preview.png"; - import styles from "./App.module.scss"; -const { intercom, showBanner, showSearch, siteUrl, title, userWay } = - whiteLabel; -const outerContainerId = "outer-container"; -const pageWrapId = "page-wrap"; +const { intercom, siteUrl, title, userWay } = whiteLabel; +export const OUTER_CONTAINER_ID = "outer-container"; export const App = () => { const history = useHistory(); - const [hamburgerOpen, setHamburgerOpen] = useState(false); - const [popUpMessage, setPopUpMessage] = useState({ - message: "", - visible: false, - type: "success", - }); const [userLocation, setUserLocation] = useState(null); useEffect(() => { @@ -47,8 +25,10 @@ export const App = () => { setUserLocation(loc); }); - ReactGA.initialize(config.GOOGLE_ANALYTICS_ID); - ReactGA_4.initialize(config.GOOGLE_ANALYTICS_GA4_ID); + if (config.GOOGLE_ANALYTICS_GA4_ID) { + ReactGA_4.initialize(config.GOOGLE_ANALYTICS_GA4_ID); + } + return history.listen((loc) => { setTimeout(() => { /* We call setTimeout here to give our views time to update the document title before @@ -59,15 +39,12 @@ export const App = () => { hitType: "pageview", page, }); - - ReactGA.set({ page }); - ReactGA.pageview(page); }, 500); }); }, [history]); return ( -
+
{title} @@ -90,24 +67,7 @@ export const App = () => { {intercom && config.INTERCOM_APP_ID && ( )} - setHamburgerOpen(s.isOpen)} - pageWrapId={pageWrapId} - toggleHamburgerMenu={() => setHamburgerOpen(!hamburgerOpen)} - /> -
- setHamburgerOpen(!hamburgerOpen)} - /> - {showBanner && } -
- -
- {popUpMessage && } -
+
); diff --git a/app/Router.tsx b/app/Router.tsx index b1a482a4a..49215dce1 100644 --- a/app/Router.tsx +++ b/app/Router.tsx @@ -6,7 +6,7 @@ import { ProtectedRoute, PublicRoute } from "components/utils"; import { AuthInterstitial } from "pages/AuthInterstitial"; import { HomePage } from "pages/HomePage"; -import { AboutPage } from "pages/AboutPage"; +import { AboutPage } from "pages/AboutPageOur415"; import { ListingDebugPage } from "pages/debug/ListingDemoPage"; import { NavigatorDashboard } from "pages/NavigatorDashboard/NavigatorDashboard"; import { OrganizationListingPage } from "pages/OrganizationListingPage"; @@ -17,7 +17,7 @@ import { } from "pages/LegacyRedirects"; import { ResourceGuides, ResourceGuide } from "pages/ResourceGuides"; import { SearchResultsPage } from "pages/SearchResultsPage/SearchResultsPage"; -import { ServiceListingPage } from "pages/ServiceListingPage"; +import { ServiceListingPage } from "pages/ServiceListingPage/ServiceListingPage"; import { ServicePdfPage } from "pages/Pdf/ServicePdfPage"; import { IntimatePartnerViolencePdfPage } from "pages/Pdf/IntimatePartnerViolencePdfPage"; import { TermsOfServicePage } from "pages/legal/TermsOfService"; @@ -30,6 +30,8 @@ import { ServiceDiscoveryResults } from "pages/ServiceDiscoveryResults"; import { LoginPage } from "pages/Auth/LoginPage"; import { SignUpPage } from "pages/Auth/SignUpPage"; import { LogoutPage } from "pages/Auth/LogoutPage"; +import { SecondaryNavigationLayout } from "components/layouts/SecondaryNavigationLayout"; +import { BackNavigation } from "components/layouts/BackNavigation"; const { homePageComponent } = whiteLabel; @@ -65,7 +67,13 @@ export const Router = ({ ( + Back} + > + + + )} /> - + ( + + Back to Services + + } + > + + + )} + /> ( +
+); diff --git a/app/components/layouts/BackNavigation.tsx b/app/components/layouts/BackNavigation.tsx new file mode 100644 index 000000000..c512a8718 --- /dev/null +++ b/app/components/layouts/BackNavigation.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +import { Button } from "components/ui/inline/Button/Button"; +import { useHistory } from "react-router-dom"; + +// Renders a smart back link that handles pages visited directly or from referring page +export const BackNavigation = ({ + defaultReturnTo, + children, +}: { + defaultReturnTo?: string; + children: string; +}) => { + const history = useHistory(); + + // The "POP" action is reserved as a default action for newly created history objects. `react-router-dom` depends on + // another history library that defines this behavior. Read more: + // https://github.com/remix-run/history/blob/main/docs/api-reference.md#reference + const backDestination = defaultReturnTo + ? () => history.push(defaultReturnTo) + : () => history.goBack(); + + return ( + + ); +}; diff --git a/app/components/layouts/SecondaryNavigation.module.scss b/app/components/layouts/SecondaryNavigation.module.scss new file mode 100644 index 000000000..8833e82e3 --- /dev/null +++ b/app/components/layouts/SecondaryNavigation.module.scss @@ -0,0 +1,16 @@ +@import "app/styles/utils/_helpers.scss"; + +.container { + background: $surface-4; + padding: $spacing-6 $spacing-16; + color: $text-tertiary; + + @media screen and (max-width: $break-tablet-s) { + padding: $spacing-6 calc-em(20px); + button { + display: flex; + align-items: center; + justify-content: flex-start; + } + } +} diff --git a/app/components/layouts/SecondaryNavigationLayout.tsx b/app/components/layouts/SecondaryNavigationLayout.tsx new file mode 100644 index 000000000..8674a525e --- /dev/null +++ b/app/components/layouts/SecondaryNavigationLayout.tsx @@ -0,0 +1,20 @@ +import React, { ReactNode } from "react"; + +import styles from "./SecondaryNavigation.module.scss"; + +// Wrap base page components in Router to add a secondary navigation bar that accepts +// an argument for rendering out a component of navigation items. +export const SecondaryNavigationLayout = ({ + children, + navigationChildren, +}: { + children: ReactNode; + navigationChildren: ReactNode | ReactNode[]; +}) => { + return ( + <> +
{navigationChildren}
+ {children} + + ); +}; diff --git a/app/components/listing/ActionBar.module.scss b/app/components/listing/ActionBar.module.scss new file mode 100644 index 000000000..8f6d18769 --- /dev/null +++ b/app/components/listing/ActionBar.module.scss @@ -0,0 +1,18 @@ +@import "../../styles/utils/helpers"; + +.action-bar-mobile { + display: none; + @include r_max($break-tablet-s) { + display: flex; + gap: $general-spacing-s; + } + &--listitem { + list-style-type: none; + display: flex; + justify-content: center; + + .action-sidebar--icon { + margin-right: $general-spacing-xs; + } + } +} diff --git a/app/components/listing/ActionBar.scss b/app/components/listing/ActionBar.scss deleted file mode 100644 index bcee2989f..000000000 --- a/app/components/listing/ActionBar.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import "../../styles/utils/helpers"; - -.action-sidebar { - li { - text-align: center; - border: 1px solid $color-grey3; - width: 110px; - height: 110px; - - a { - color: $color-textfade; - padding-top: $padding-default; - display: flex; - gap: 12px; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - - img { - height: 36px; - width: 36px; - margin: 0 auto; - } - - span { - font-size: $padding-large; - color: $color-grey6; - line-height: 1.2; - } - } - - &:hover { - background: $color-grey3; - color: $color-title; - } - } -} - -.mobile-action-bar { - display: none; - @include r_max($break-tablet-s) { - display: flex; - justify-content: space-between; - padding: calc-em(16px) calc-em(4px); - border-top: 1px solid #dddddd; - border-bottom: 1px solid #dddddd; - } - &--listitem { - list-style-type: none; - display: flex; - justify-content: center; - - .action-sidebar--icon { - margin-right: calc-em(8px); - } - } -} diff --git a/app/components/listing/ActionBar.tsx b/app/components/listing/ActionBar.tsx index 15325c05a..2adff2e2f 100644 --- a/app/components/listing/ActionBar.tsx +++ b/app/components/listing/ActionBar.tsx @@ -1,81 +1,55 @@ import React from "react"; -import { Link } from "react-router-dom"; -import { icon as iconPath } from "../../assets"; +import { Button } from "components/ui/inline/Button/Button"; import { OrganizationAction } from "../../models"; -import "./ActionBar.scss"; +import styles from "./ActionBar.module.scss"; const ActionButton = ({ action, onClickAction, - iconColor, - listItemClass, }: { action: OrganizationAction; - iconColor: string; - listItemClass?: string; onClickAction: (a: OrganizationAction) => void; }) => { const { icon, link, name, to } = action; - const linkClass = `action-sidebar--${name.toLowerCase()}`; - const content = ( - <> - {icon} - {name} - - ); return ( -
  • - {to ? ( - - {content} - - ) : ( - { - onClickAction(action); - }} - rel="noopener noreferrer" - target="_blank" - > - {content} - - )} -
  • + ); }; export const ActionSidebar = ({ actions, onClickAction }: ActionBarProps) => ( -
      + <> {actions.map((action) => ( ))} -
    + ); export const ActionBarMobile = ({ actions, onClickAction }: ActionBarProps) => ( -
      +
      {actions.map((action) => ( ))} -
    +
    ); export interface ActionBarProps { diff --git a/app/components/listing/ContactInfoTableRows.tsx b/app/components/listing/ContactInfoTableRows.tsx new file mode 100644 index 000000000..73be79f53 --- /dev/null +++ b/app/components/listing/ContactInfoTableRows.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { Service } from "../../models"; + +export const ContactInfoTableRows = ({ service }: { service: Service }) => { + const website = service.url || service.resource?.website; + const email = service.email || service.resource?.email; + const phones = service.resource?.phones; + + if (!website && !phones && !email) { + return ( + + + It seems like we have no contact info on record, please click edit and + add it if you can! + + + ); + } + + return ( + <> + {website && ( + + Website + + + {website} + + + + )} + + {email && ( + + Email + + {email} + + + )} + + {phones?.length && ( + + Phone + +
      + {phones.map((phone) => ( +
    • + {phone.number}{" "} + {phone.service_type && `(${phone.service_type})`} +
    • + ))} +
    + + + )} + + ); +}; diff --git a/app/components/listing/LabelTagRows.module.scss b/app/components/listing/LabelTagRows.module.scss new file mode 100644 index 000000000..349899081 --- /dev/null +++ b/app/components/listing/LabelTagRows.module.scss @@ -0,0 +1,10 @@ +@import "../../styles/utils/helpers"; + +.labelTags { + display: flex; + gap: $general-spacing-s; + flex-wrap: wrap; + @include r_max($break-tablet-s) { + gap: $general-spacing-xs; + } +} diff --git a/app/components/listing/LabelTagRows.tsx b/app/components/listing/LabelTagRows.tsx new file mode 100644 index 000000000..723231f00 --- /dev/null +++ b/app/components/listing/LabelTagRows.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { LabelTag } from "components/ui/LabelTag"; +import { Service } from "../../models"; +import styles from "./LabelTagRows.module.scss"; + +const LabelTagRows = ({ service }: { service: Service }) => { + const topLevelCategories = service.categories?.filter( + (srv) => srv.top_level === true + ); + const subcategories = service.categories?.filter( + (srv) => srv.top_level === false + ); + + return ( + <> + {topLevelCategories.length > 0 && ( + + Categories + + {topLevelCategories.map((srv) => ( + + ))} + + + )} + + {subcategories.length > 0 && ( + + Subcategories + + {subcategories.map((srv) => ( + + ))} + + + )} + + {service.eligibilities.length > 0 && ( + + Eligibilities + + {service.eligibilities.map((srv) => ( + + ))} + + + )} + + ); +}; + +export default LabelTagRows; diff --git a/app/components/listing/ListingInfoTable.module.scss b/app/components/listing/ListingInfoTable.module.scss new file mode 100644 index 000000000..0df4955fb --- /dev/null +++ b/app/components/listing/ListingInfoTable.module.scss @@ -0,0 +1,29 @@ +@import "../../styles/utils/_helpers.scss"; + +.listingInfoTable { + width: 100%; + + tr { + transition: all 0.2s ease-in-out; + + th, + td { + padding: 10px; + text-align: left; + } + + th { + font-family: "Montserrat"; + font-size: 18px; + font-weight: bold; + color: $text-primary; + width: 150px; + padding-right: $general-spacing-lg; + padding-left: $spacing-0; + @include r_max($break-tablet-s) { + width: 100px; + font-size: 16px; + } + } + } +} diff --git a/app/components/listing/ListingInfoTable.tsx b/app/components/listing/ListingInfoTable.tsx new file mode 100644 index 000000000..ed2fbe964 --- /dev/null +++ b/app/components/listing/ListingInfoTable.tsx @@ -0,0 +1,25 @@ +import React, { ReactNode } from "react"; +import styles from "./ListingInfoTable.module.scss"; + +interface ListingInfoTableProps { + rows?: T[]; + rowRenderer?: (row: T) => JSX.Element; + children?: ReactNode; +} + +export const ListingInfoTable = ({ + rows, + rowRenderer, + children, +}: ListingInfoTableProps) => { + const useRowRenderer = !children && rows && rowRenderer; + + return ( + + + {children} + {useRowRenderer && rows.map((row) => rowRenderer(row))} + +
    + ); +}; diff --git a/app/components/listing/ListingTitleLink.tsx b/app/components/listing/ListingTitleLink.tsx deleted file mode 100644 index 90a516c8a..000000000 --- a/app/components/listing/ListingTitleLink.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { BrowserRouter, Link } from "react-router-dom"; -import { Tooltip } from "react-tippy"; -import "react-tippy/dist/tippy.css"; -import { OrganizationCard } from "./OrganizationCard"; -import { ServiceCard } from "./ServiceCard"; -import { Service, Organization } from "../../models"; - -export const ListingTitleLink = ({ - listing, - type, -}: - | { listing: Service; type: "service" } - | { listing: Organization; type: "org" }) => { - const isService = type === "service"; - const to = isService - ? `/services/${listing.id}` - : `/organizations/${listing.id}`; - const summaryCard = - type === "service" ? ( - - ) : ( - - ); - - return ( - {summaryCard}} - theme="light" - > - {listing.name} - - ); -}; diff --git a/app/components/listing/MapOfLocations.module.scss b/app/components/listing/MapOfLocations.module.scss new file mode 100644 index 000000000..52297e81f --- /dev/null +++ b/app/components/listing/MapOfLocations.module.scss @@ -0,0 +1,6 @@ +@import "../../styles/utils/_helpers.scss"; + +.locationsMap { + border: 1px solid $border-lightgray; + border-radius: $rounded-md; +} diff --git a/app/components/listing/MapOfLocations.tsx b/app/components/listing/MapOfLocations.tsx index 9cb78fa05..c252b9257 100644 --- a/app/components/listing/MapOfLocations.tsx +++ b/app/components/listing/MapOfLocations.tsx @@ -10,6 +10,9 @@ import { UserLocationMarker, } from "../ui/MapElements"; import { useAppContext } from "../../utils"; +import styles from "./MapOfLocations.module.scss"; + +// TODO: Accordion needs big refactor/rebuild which is out of scope of this ticket. Will create new ticket. export const MapOfLocations = ({ locationRenderer, @@ -25,7 +28,7 @@ export const MapOfLocations = ({ const { lat, lng } = userLocation; return ( -
    +
    - - - - - - - - -
    {i + 1}. - - {loc.address.address_1} - - -
    - - keyboard_arrow_down - -
    -
    - {/* TODO Transportation options */} -
    + + + + + + + +
    + +

    {`${i + 1}. ${loc.address.address_1}`}

    +
    +
    +
    + keyboard_arrow_down +
    +
    } > {locationRenderer(loc)} @@ -81,17 +78,6 @@ export const MapOfLocations = ({ ))} )} - {/* - - { locations.map((loc, i) => ( - - - - - - )) } - -
    { i }.{ loc.address.address_1 }
    */}
    ); }; diff --git a/app/components/listing/Notes.tsx b/app/components/listing/NotesList.tsx similarity index 54% rename from app/components/listing/Notes.tsx rename to app/components/listing/NotesList.tsx index 793017dcc..062010a10 100644 --- a/app/components/listing/Notes.tsx +++ b/app/components/listing/NotesList.tsx @@ -2,7 +2,7 @@ import React from "react"; import ReactMarkdown from "react-markdown"; import { Note } from "../../models"; -const NotesList = ({ notes }: { notes: Note[] }) => ( +export const NotesList = ({ notes }: { notes: Note[] }) => (
      {notes.map((noteObj) => (
    • @@ -13,12 +13,3 @@ const NotesList = ({ notes }: { notes: Note[] }) => ( ))}
    ); - -export const Notes = ({ id, notes }: { id?: string; notes: Note[] }) => ( -
    -
    -

    Notes

    -
    - -
    -); diff --git a/app/components/listing/PageHeader.module.scss b/app/components/listing/PageHeader.module.scss new file mode 100644 index 000000000..a03685338 --- /dev/null +++ b/app/components/listing/PageHeader.module.scss @@ -0,0 +1,13 @@ +@import "../../styles/utils/_helpers.scss"; + +.pageHeader { + h1 { + color: $text-primary; + font-size: 3rem; + text-transform: capitalize; + margin-bottom: $padding-default; + @include r_max($break-tablet-s) { + font-size: 2rem; + } + } +} diff --git a/app/components/listing/PageHeader.tsx b/app/components/listing/PageHeader.tsx new file mode 100644 index 000000000..5a4b8e563 --- /dev/null +++ b/app/components/listing/PageHeader.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import styles from "./PageHeader.module.scss"; + +type ListingPageHeaderProps = { + title: string; + children?: React.ReactNode; + dataCy: string; +}; + +const ListingPageHeader = ({ + title, + children, + dataCy, +}: ListingPageHeaderProps) => ( +
    +

    + {title} +

    + {children} +
    +); + +export default ListingPageHeader; diff --git a/app/components/listing/PageWrapper.module.scss b/app/components/listing/PageWrapper.module.scss new file mode 100644 index 000000000..bf4efc996 --- /dev/null +++ b/app/components/listing/PageWrapper.module.scss @@ -0,0 +1,82 @@ +@import "../../styles/utils/_helpers.scss"; + +.listing-wrapper { + background: $color-grey1; + padding: $section-padding-desktop-vertical $general-spacing-md; + background: $color-white; + + @include r_max($break-tablet-s) { + padding: $section-padding-mobile-vertical $general-spacing-md; + } + + header { + margin-bottom: $general-spacing-lg; + } +} + +.listing { + color: $text-secondary; + + p { + padding-left: $spacing-0; + line-height: 150%; + } + + a { + color: $link-blue-default; + + &:hover { + color: $link-blue-hover; + } + } + + p, + a, + table { + @include r_max($break-tablet-s) { + font-size: 14px; + line-height: 125%; + } + } +} + +.listing--main { + width: 100%; + margin: auto; + display: flex; + justify-content: center; + + &--left { + padding-right: $padding-large; + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + align-content: stretch; + + @include r_min($break-tablet-s) { + width: calc-em(672px); + } + } + + @include r_max($break-tablet-s) { + display: block; + margin-top: 0; + &--left { + padding-right: 0; + } + } +} + +.listing--aside { + max-height: 100vh; + display: flex; + flex-direction: column; + align-self: start; + gap: 1rem; + width: fit-content; + + @include r_max($break-tablet-s) { + display: none; + } +} diff --git a/app/components/listing/PageWrapper.tsx b/app/components/listing/PageWrapper.tsx new file mode 100644 index 000000000..59a8eeef4 --- /dev/null +++ b/app/components/listing/PageWrapper.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Helmet } from "react-helmet-async"; +import { Footer } from "components/ui"; +import { ActionSidebar } from "components/listing"; +import styles from "./PageWrapper.module.scss"; + +type ListingPageWrapperProps = { + title: string; + description: string; + children: React.ReactNode; + sidebarActions: any[]; + onClickAction: (action: any) => void; +}; + +const ListingPageWrapper = ({ + title, + description, + children, + sidebarActions, + onClickAction, +}: ListingPageWrapperProps) => ( + <> +
    + + {title} + + +
    +
    +
    {children}
    + +
    +
    +
    +