diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index e364fb86c..6aef29b0d 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). The format is based on the `RELEASE-NOTES-TEMPLATE.md` file. +## Release 2.15.0 + +## Introduction +* Product name: Open Supply Hub +* Release date: November 8, 2025 + +### What's new +* [OSDEV-2200](https://opensupplyhub.atlassian.net/browse/OSDEV-2200) - Implements a new claim introduction page for the new facility claiming process, accessible via `/claim/:osId`, which can be enabled or activated through a feature flag. + +### Release instructions +* Ensure that the following commands are included in the `post_deployment` command: + * `migrate` + * `reindex_database` + + ## Release 2.14.0 ## Introduction diff --git a/src/react/src/Routes.jsx b/src/react/src/Routes.jsx index db60fbf56..eb84a7ead 100644 --- a/src/react/src/Routes.jsx +++ b/src/react/src/Routes.jsx @@ -35,6 +35,7 @@ import SearchByOsIdResult from './components/Contribute/SearchByOsIdResult'; import SearchByNameAndAddressResult from './components/Contribute/SearchByNameAndAddressResult'; import ProductionLocationInfo from './components/Contribute/ProductionLocationInfo'; import withProductionLocationSubmit from './components/Contribute/HOC/withProductionLocationSubmit'; +import ClaimIntro from './components/InitialClaimFlow/ClaimIntro/ClaimIntro'; import { sessionLogin } from './actions/auth'; import { fetchFeatureFlags } from './actions/featureFlags'; @@ -55,8 +56,10 @@ import { facilitiesRoute, dashboardRoute, claimFacilityRoute, + claimIntroRoute, claimedFacilitiesRoute, CLAIM_A_FACILITY, + ENABLE_V1_CLAIMS_FLOW, settingsRoute, InfoLink, InfoPaths, @@ -106,6 +109,20 @@ class Routes extends Component { id="mainPanel" > + ( + + } + > + + + )} + /> { + const renderComponent = () => renderWithProviders(); + + test('renders without crashing', () => { + renderComponent(); + }); + + describe('Step 1 - Eligibility rendering', () => { + test('displays Step 1 title', () => { + const { getByText } = renderComponent(); + + expect(getByText('Confirm Your Eligibility')).toBeInTheDocument(); + }); + + test('displays eligibility requirements', () => { + const { getByText } = renderComponent(); + + expect( + getByText(/Claim requests must be submitted by a current employee/) + ).toBeInTheDocument(); + expect( + getByText(/If you're not an owner or manager/) + ).toBeInTheDocument(); + }); + }); + + describe('Step 2 - Prove Your Name and Role rendering', () => { + test('displays Step 2 title', () => { + const { getByText } = renderComponent(); + + expect(getByText('Prove Your Name and Role')).toBeInTheDocument(); + }); + + test('displays OPTIONS list', () => { + const { getByText } = renderComponent(); + + expect( + getByText(/Company website showing your name and role/) + ).toBeInTheDocument(); + expect(getByText(/Employee ID badge/)).toBeInTheDocument(); + expect(getByText(/Employment letter/)).toBeInTheDocument(); + }); + + test('displays employee examples for Step 2', () => { + const { getAllByAltText } = renderComponent(); + + expect( + getAllByAltText('Example employee ID badge')[0] + ).toBeInTheDocument(); + expect( + getAllByAltText('Example employment letter')[0] + ).toBeInTheDocument(); + expect( + getAllByAltText('Example business card')[0] + ).toBeInTheDocument(); + }); + }); + + describe('Step 3 - Prove Your Company Name and Address rendering', () => { + test('displays Step 3 title', () => { + const { getByText } = renderComponent(); + + expect( + getByText('Prove Your Company Name and Address') + ).toBeInTheDocument(); + }); + + test('displays company verification OPTIONS', () => { + const { getByText } = renderComponent(); + + expect(getByText(/Business registration/)).toBeInTheDocument(); + expect(getByText(/Business license/)).toBeInTheDocument(); + expect(getByText(/Utility bill/)).toBeInTheDocument(); + }); + + test('displays NOTE about company name and address', () => { + const { getByText } = renderComponent(); + + expect( + getByText(/The document must show the same company name and address/) + ).toBeInTheDocument(); + }); + + test('displays company examples for Step 3', () => { + const { getAllByAltText } = renderComponent(); + + expect( + getAllByAltText('Example business registration certificate')[0] + ).toBeInTheDocument(); + expect( + getAllByAltText('Example business license')[0] + ).toBeInTheDocument(); + expect( + getAllByAltText('Example utility bill')[0] + ).toBeInTheDocument(); + }); + }); + + describe('Step 4 and 5 - Maximum Value section rendering', () => { + test('displays Maximum Value badge', () => { + const { getByText } = renderComponent(); + + expect(getByText('Maximum Value')).toBeInTheDocument(); + }); + + test('displays Step 4 - Add Key Details', () => { + const { getByText } = renderComponent(); + + expect(getByText(/Add Key Details:/)).toBeInTheDocument(); + expect( + getByText(/Provide information about the production location/) + ).toBeInTheDocument(); + }); + + test('displays Step 5 - Get Verified', () => { + const { getByText } = renderComponent(); + + expect(getByText(/Get Verified:/)).toBeInTheDocument(); + expect( + getByText(/After the claim is approved, you get a credible/) + ).toBeInTheDocument(); + }); + }); + + describe('Important Note rendering', () => { + test('displays important warning box', () => { + const { getByText } = renderComponent(); + + expect(getByText('IMPORTANT!')).toBeInTheDocument(); + expect( + getByText(/Any documentation appearing to be forged/) + ).toBeInTheDocument(); + }); + + test('displays info icon in warning box', () => { + const { container } = renderComponent(); + + const svgIcons = container.querySelectorAll('svg'); + expect(svgIcons.length).toBeGreaterThan(0); + }); + }); + + describe('Image Dialog functionality', () => { + test('opens dialog when example image is clicked', async () => { + const { getAllByAltText, getByRole } = renderComponent(); + + const exampleImage = getAllByAltText('Example employee ID badge')[0]; + const imageButton = exampleImage.closest('button'); + + fireEvent.click(imageButton); + + await waitFor(() => { + const dialog = getByRole('dialog'); + expect(dialog).toBeInTheDocument(); + }); + }); + + test('displays image in dialog when opened', async () => { + const { getAllByAltText } = renderComponent(); + + const exampleImage = getAllByAltText('Example employee ID badge')[0]; + const imageButton = exampleImage.closest('button'); + + fireEvent.click(imageButton); + + await waitFor(() => { + const dialogImages = getAllByAltText('Example employee ID badge'); + + expect(dialogImages.length).toBeGreaterThan(1); + }); + }); + }); + + describe('External link', () => { + test('displays learn more link', () => { + const { getByText } = renderComponent(); + + const link = getByText('Learn more about claiming your production location'); + expect(link).toBeInTheDocument(); + expect(link.tagName).toBe('A'); + expect(link).toHaveAttribute('href', 'https://info.opensupplyhub.org/resources/claim-a-facility'); + expect(link).toHaveAttribute('target', '_blank'); + }); + }); +}); diff --git a/src/react/src/__tests__/components/ClaimIntro.test.js b/src/react/src/__tests__/components/ClaimIntro.test.js new file mode 100644 index 000000000..f03ad09d5 --- /dev/null +++ b/src/react/src/__tests__/components/ClaimIntro.test.js @@ -0,0 +1,125 @@ +import React from 'react'; +import { Router } from 'react-router-dom'; +import { fireEvent } from '@testing-library/react'; +import history from '../../util/history'; +import renderWithProviders from '../../util/testUtils/renderWithProviders'; +import ClaimIntro from '../../components/InitialClaimFlow/ClaimIntro/ClaimIntro'; +import { makeClaimDetailsLink } from '../../util/util'; + +jest.mock('../../components/InitialClaimFlow/ClaimIntro/ClaimInfoSection', () => () => ( +
ClaimInfoSection
+)); + +describe('ClaimIntro component', () => { + const mockOsID = 'TEST123'; + const mockMatch = { + params: { osID: mockOsID }, + }; + + const renderComponent = (userHasSignedIn = true) => { + const preloadedState = { + auth: { + user: { + user: { + isAnon: !userHasSignedIn, + }, + }, + }, + }; + + return renderWithProviders( + + + , + { preloadedState } + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + history.push('/'); + }); + + describe('Authentication checks', () => { + test('renders RequireAuthNotice when user is not signed in', () => { + const { getByText } = renderComponent(false); + + expect(getByText('Claim this production location')).toBeInTheDocument(); + expect( + getByText('Log in to claim a production location on Open Supply Hub') + ).toBeInTheDocument(); + }); + + test('renders main content when user is signed in', () => { + const { getByText, getByTestId } = renderComponent(true); + + expect(getByText('Claim a Production Location')).toBeInTheDocument(); + expect(getByTestId('claim-info-section')).toBeInTheDocument(); + }); + }); + + describe('Component rendering', () => { + test('renders without crashing', () => { + renderComponent(); + }); + + test('displays the correct title', () => { + const { getByText } = renderComponent(); + + expect(getByText('Claim a Production Location')).toBeInTheDocument(); + }); + + test('displays the subtitle text', () => { + const { getByText } = renderComponent(); + + expect( + getByText(/In order to submit a claim request/) + ).toBeInTheDocument(); + }); + + test('renders both action buttons', () => { + const { getByText } = renderComponent(); + + expect(getByText('GO BACK')).toBeInTheDocument(); + expect(getByText('Continue to Claim Form')).toBeInTheDocument(); + }); + + test('renders ClaimInfoSection component', () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('claim-info-section')).toBeInTheDocument(); + }); + }); + + describe('Navigation functionality', () => { + test('navigates back when GO BACK button is clicked', () => { + history.push('/some-page'); + const previousPath = history.location.pathname; + + const { getByText } = renderComponent(); + const backButton = getByText('GO BACK'); + + fireEvent.click(backButton); + + expect(history.location.pathname).toBe(previousPath); + }); + + test('navigates to claim details when Continue button is clicked', () => { + const { getByText } = renderComponent(); + const continueButton = getByText('Continue to Claim Form'); + + fireEvent.click(continueButton); + + const expectedPath = makeClaimDetailsLink(mockOsID); + expect(history.location.pathname).toBe(expectedPath); + }); + }); + + describe('Props validation', () => { + test('receives osID from route params', () => { + const { container } = renderComponent(); + + expect(container.firstChild).toBeInTheDocument(); + }); + }); +}); diff --git a/src/react/src/components/InitialClaimFlow/ClaimIntro/ClaimInfoSection.jsx b/src/react/src/components/InitialClaimFlow/ClaimIntro/ClaimInfoSection.jsx new file mode 100644 index 000000000..eeb278ab7 --- /dev/null +++ b/src/react/src/components/InitialClaimFlow/ClaimIntro/ClaimInfoSection.jsx @@ -0,0 +1,242 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import InfoIcon from '@material-ui/icons/Info'; +import StarIcon from '@material-ui/icons/Star'; +import COLOURS from '../../../util/COLOURS'; +import { ClaimFacilityInfoLink } from '../../../util/constants'; +import { claimInfoStyles } from './styles'; +import ExampleImage from './ExampleImage'; + +import businessRegistrationExample from '../../../images/business-registration-example.jpg'; +import businessLicenseExample from '../../../images/business-license-example.jpg'; +import utilityBillExample from '../../../images/utility-bill-example.jpg'; +import employeeIdExample from '../../../images/employee-id-example.jpg'; +import employmentLetterExample from '../../../images/employment-letter-example.jpg'; +import businessCardExample from '../../../images/business-card-example.jpg'; + +const ClaimInfoSection = ({ classes }) => ( +
+
+
+
+ 1 +
+
+ + Confirm Your Eligibility + +
+
+ + + Claim requests must be submitted by a current + employee of the location or its parent company. + +
+
+ + + If you're not an owner or manager, you can + still proceed by providing your + supervisor's contact information for + verification. + +
+
+
+
+
+
+
+
+
+ 2 +
+ + Prove Your Name and Role + +
+ + OPTIONS: Company website showing your name + and role, Employee ID badge, Employment letter, Job + contract, Link to your LinkedIn profile, Business card, + Audit reports. + +
+ + + +
+
+
+
+
+ 3 +
+ + Prove Your Company Name and Address + +
+ + OPTIONS: Business registration, Business + license, Utility bill, link to company website, link to + company LinkedIn page. + +
+ + NOTE: The document must show the same + company name and address as listed on your OS Hub + profile. + +
+
+ + + +
+
+
+
+
+ + + Maximum Value + +
+ +
+
+ 4 +
+ + Add Key Details:{' '} + Provide information about the production location such as + Certifications, Number of Workers, Contact Information, and + more. + +
+ +
+
+ 5 +
+ + Get Verified:{' '} + After the claim is approved, you get a credible and + confirmed profile—with a green banner and Claimed badge—that + helps buyers trust and find your company.{' '} + + Learn more about claiming your production location + + +
+
+
+ + + + IMPORTANT! + {' '} + Any documentation appearing to be forged or counterfeit may + result in your claim request being denied. + +
+
+); + +ClaimInfoSection.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(claimInfoStyles)(ClaimInfoSection); diff --git a/src/react/src/components/InitialClaimFlow/ClaimIntro/ClaimIntro.jsx b/src/react/src/components/InitialClaimFlow/ClaimIntro/ClaimIntro.jsx new file mode 100644 index 000000000..991906816 --- /dev/null +++ b/src/react/src/components/InitialClaimFlow/ClaimIntro/ClaimIntro.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { withRouter } from 'react-router-dom'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; + +import ClaimInfoSection from './ClaimInfoSection'; +import AppGrid from '../../AppGrid'; +import AppOverflow from '../../AppOverflow'; +import { makeClaimDetailsLink } from '../../../util/util'; +import RequireAuthNotice from '../../RequireAuthNotice'; +import { claimIntroStyles } from './styles'; + +const ClaimIntro = ({ classes, history, osID, userHasSignedIn }) => { + if (!userHasSignedIn) { + return ( + + ); + } + + const handleGoBack = () => { + history.goBack(); + }; + + const handleContinue = () => { + history.push(makeClaimDetailsLink(osID)); + }; + + return ( +
+ + +
+
+ + Claim a Production Location + + + In order to submit a claim request, you must be + an owner, manager, have supervisor approval or + be an authorized representative of the parent + company of the production location. + +
+ +
+
+ + +
+
+
+
+
+
+ ); +}; + +ClaimIntro.propTypes = { + osID: PropTypes.string.isRequired, + userHasSignedIn: PropTypes.bool.isRequired, + classes: PropTypes.object.isRequired, + history: PropTypes.shape({ + goBack: PropTypes.func.isRequired, + push: PropTypes.func.isRequired, + }).isRequired, +}; + +const mapStateToProps = ( + { + auth: { + user: { user }, + }, + }, + { + match: { + params: { osID }, + }, + }, +) => ({ + osID, + userHasSignedIn: !user.isAnon, +}); + +export default connect(mapStateToProps)( + withRouter(withStyles(claimIntroStyles)(ClaimIntro)), +); diff --git a/src/react/src/components/InitialClaimFlow/ClaimIntro/ExampleImage.jsx b/src/react/src/components/InitialClaimFlow/ClaimIntro/ExampleImage.jsx new file mode 100644 index 000000000..a52836dda --- /dev/null +++ b/src/react/src/components/InitialClaimFlow/ClaimIntro/ExampleImage.jsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import ImageDialog from './ImageDialog'; +import { claimInfoStyles } from './styles'; + +const ExampleImage = ({ + src, + alt, + label, + borderClass, + labelColorClass, + classes, +}) => { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( +
+ + + {label} + + setDialogOpen(false)} + image={src} + alt={alt} + /> +
+ ); +}; + +ExampleImage.propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + borderClass: PropTypes.string.isRequired, + labelColorClass: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, +}; + +export default withStyles(claimInfoStyles)(ExampleImage); diff --git a/src/react/src/components/InitialClaimFlow/ClaimIntro/ImageDialog.jsx b/src/react/src/components/InitialClaimFlow/ClaimIntro/ImageDialog.jsx new file mode 100644 index 000000000..0ca64fe0d --- /dev/null +++ b/src/react/src/components/InitialClaimFlow/ClaimIntro/ImageDialog.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import IconButton from '@material-ui/core/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import { claimInfoStyles } from './styles'; + +const ImageDialog = ({ open, onClose, image, alt, classes }) => ( + + + + + + {alt} + + +); + +ImageDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + image: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, +}; + +export default withStyles(claimInfoStyles)(ImageDialog); diff --git a/src/react/src/components/InitialClaimFlow/ClaimIntro/styles.js b/src/react/src/components/InitialClaimFlow/ClaimIntro/styles.js new file mode 100644 index 000000000..efbce54cf --- /dev/null +++ b/src/react/src/components/InitialClaimFlow/ClaimIntro/styles.js @@ -0,0 +1,297 @@ +import COLOURS from '../../../util/COLOURS'; + +export const claimIntroStyles = theme => ({ + root: { + backgroundColor: COLOURS.LIGHT_GREY, + minHeight: 'calc(100vh - 64px)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + paddingTop: theme.spacing.unit * 0, + paddingBottom: theme.spacing.unit * 4, + }, + container: { + maxWidth: 1440, + width: '100%', + padding: theme.spacing.unit * 2, + [theme.breakpoints.down('sm')]: { + padding: theme.spacing.unit * 2, + }, + }, + heroSection: { + textAlign: 'center', + marginBottom: theme.spacing.unit * 3, + marginTop: 0, + paddingTop: 0, + }, + title: { + fontSize: 48, + fontWeight: 700, + color: COLOURS.JET_BLACK, + marginBottom: theme.spacing.unit, + [theme.breakpoints.down('sm')]: { + fontSize: 24, + }, + }, + subtitle: { + fontSize: 18, + color: COLOURS.MEDIUM_GREY, + maxWidth: 720, + margin: '0 auto', + lineHeight: 1.6, + }, + actionsContainer: { + backgroundColor: COLOURS.WHITE, + padding: theme.spacing.unit * 2, + marginTop: theme.spacing.unit * 3, + marginBottom: 0, + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + }, + actionsInner: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + gap: theme.spacing.unit * 2, + }, + }, + backButton: { + padding: '10px 24px', + fontSize: 16, + fontWeight: 800, + borderColor: COLOURS.LIGHT_BORDER, + textTransform: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + '&:hover': { + backgroundColor: COLOURS.HOVER_GREY, + borderColor: COLOURS.MEDIUM_BORDER, + }, + [theme.breakpoints.down('sm')]: { + width: '100%', + }, + }, + continueButton: { + padding: '10px 24px', + fontSize: 18, + fontWeight: 800, + backgroundColor: theme.palette.action.main, + color: COLOURS.BLACK, + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + textTransform: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + '&:hover': { + backgroundColor: theme.palette.action.dark, + boxShadow: '0 4px 8px rgba(0,0,0,0.15)', + }, + [theme.breakpoints.down('sm')]: { + width: '100%', + }, + }, + icon: { + marginLeft: theme.spacing.unit, + fontSize: 20, + fontWeight: 800, + display: 'flex', + alignItems: 'center', + }, +}); + +export const claimInfoStyles = theme => ({ + root: { + backgroundColor: COLOURS.WHITE, + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + padding: theme.spacing.unit * 3, + '& > *:not(:last-child)': { + marginBottom: theme.spacing.unit * 3, + }, + }, + stepBox: { + padding: theme.spacing.unit * 2, + marginBottom: 0, + display: 'flex', + flexDirection: 'column', + }, + blueStep: { + backgroundColor: COLOURS.EXTRA_LIGHT_BLUE, + border: `1px solid ${COLOURS.LIGHT_BLUE_BORDER}`, + }, + purpleStep: { + backgroundColor: COLOURS.LIGHT_PURPLE_BG, + border: `1px solid ${COLOURS.LIGHT_PURPLE_BORDER}`, + }, + greenStep: { + backgroundColor: COLOURS.LIGHT_GREEN, + border: `1px solid ${COLOURS.LIGHT_GREEN_BORDER}`, + }, + amberStep: { + background: COLOURS.LIGHT_AMBER, + border: `1px solid ${COLOURS.AMBER}`, + boxShadow: '0 4px 12px rgba(255, 193, 7, 0.3)', + }, + stepNumber: { + width: 32, + height: 32, + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: COLOURS.WHITE, + fontWeight: 600, + fontSize: 18, + marginRight: theme.spacing.unit * 1.5, + flexShrink: 0, + }, + blueNumber: { + backgroundColor: COLOURS.MATERIAL_BLUE, + }, + purpleNumber: { + backgroundColor: COLOURS.MATERIAL_PURPLE, + }, + greenNumber: { + backgroundColor: COLOURS.MATERIAL_GREEN, + }, + amberNumber: { + background: COLOURS.ORANGE, + boxShadow: '0 2px 8px rgba(245, 124, 0, 0.4)', + }, + stepHeader: { + display: 'flex', + alignItems: 'flex-start', + marginBottom: theme.spacing.unit * 1.5, + }, + stepTitle: { + fontSize: 24, + fontWeight: 600, + marginBottom: theme.spacing.unit * 0.5, + }, + stepText: { + fontSize: 18, + lineHeight: 1.6, + }, + bulletPoint: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing.unit, + }, + bullet: { + width: 6, + height: 6, + borderRadius: '50%', + marginRight: theme.spacing.unit, + flexShrink: 0, + }, + blueBullet: { + backgroundColor: COLOURS.MATERIAL_BLUE, + }, + examplesContainer: { + display: 'flex', + gap: theme.spacing.unit * 3, + marginTop: 'auto', + paddingTop: theme.spacing.unit * 2, + justifyContent: 'flex-start', + alignItems: 'flex-start', + flexWrap: 'wrap', + }, + exampleItem: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: 80, + justifyContent: 'flex-start', + }, + exampleImage: { + width: 64, + height: 64, + objectFit: 'cover', + borderRadius: theme.spacing.unit, + cursor: 'pointer', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + transition: 'all 0.2s', + '&:hover': { + boxShadow: '0 4px 8px rgba(0,0,0,0.15)', + transform: 'translateY(-2px)', + }, + }, + purpleBorder: { + border: `2px solid ${COLOURS.LIGHT_PURPLE_BORDER}`, + }, + greenBorder: { + border: `2px solid ${COLOURS.LIGHT_GREEN_BORDER}`, + }, + exampleLabel: { + fontSize: 14, + textAlign: 'center', + marginTop: theme.spacing.unit, + fontWeight: 700, + lineHeight: 1.2, + }, + purpleLabel: { + color: COLOURS.PURPLE_TEXT, + }, + greenLabel: { + color: COLOURS.GREEN_TEXT, + }, + noteBox: { + backgroundColor: COLOURS.NOTE_GREEN, + padding: theme.spacing.unit * 1.5, + marginTop: theme.spacing.unit, + }, + maxValueBadge: { + display: 'inline-flex', + alignItems: 'center', + backgroundColor: COLOURS.ORANGE, + color: COLOURS.WHITE, + padding: '6px 12px', + borderRadius: 20, + fontSize: 18, + fontWeight: 700, + marginBottom: theme.spacing.unit * 1.5, + boxShadow: '0 2px 6px rgba(255, 160, 0, 0.4)', + }, + warningBox: { + backgroundColor: COLOURS.LIGHT_GREY, + padding: theme.spacing.unit * 1.5, + display: 'flex', + alignItems: 'center', + }, + warningIcon: { + color: COLOURS.MATERIAL_RED, + marginRight: theme.spacing.unit, + fontSize: 16, + flexShrink: 0, + }, + link: { + color: COLOURS.MATERIAL_BLUE, + textDecoration: 'none', + fontWeight: 500, + '&:hover': { + textDecoration: 'underline', + }, + }, + dialogImage: { + width: '100%', + height: 'auto', + maxHeight: '80vh', + objectFit: 'contain', + }, + closeButton: { + position: 'absolute', + right: theme.spacing.unit, + top: theme.spacing.unit, + color: theme.palette.grey[500], + }, + twoColumnGrid: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + columnGap: theme.spacing.unit * 3, + rowGap: theme.spacing.unit * 2, + [theme.breakpoints.down('sm')]: { + gridTemplateColumns: '1fr', + }, + }, +}); diff --git a/src/react/src/images/business-card-example.jpg b/src/react/src/images/business-card-example.jpg new file mode 100644 index 000000000..34732d641 Binary files /dev/null and b/src/react/src/images/business-card-example.jpg differ diff --git a/src/react/src/images/business-license-example.jpg b/src/react/src/images/business-license-example.jpg new file mode 100644 index 000000000..918dc875f Binary files /dev/null and b/src/react/src/images/business-license-example.jpg differ diff --git a/src/react/src/images/business-registration-example.jpg b/src/react/src/images/business-registration-example.jpg new file mode 100644 index 000000000..94e2aeae6 Binary files /dev/null and b/src/react/src/images/business-registration-example.jpg differ diff --git a/src/react/src/images/employee-id-example.jpg b/src/react/src/images/employee-id-example.jpg new file mode 100644 index 000000000..c12efa435 Binary files /dev/null and b/src/react/src/images/employee-id-example.jpg differ diff --git a/src/react/src/images/employment-letter-example.jpg b/src/react/src/images/employment-letter-example.jpg new file mode 100644 index 000000000..456f1b5ae Binary files /dev/null and b/src/react/src/images/employment-letter-example.jpg differ diff --git a/src/react/src/images/utility-bill-example.jpg b/src/react/src/images/utility-bill-example.jpg new file mode 100644 index 000000000..fca679a98 Binary files /dev/null and b/src/react/src/images/utility-bill-example.jpg differ diff --git a/src/react/src/util/COLOURS.js b/src/react/src/util/COLOURS.js index b008a24e1..f2a2087e2 100644 --- a/src/react/src/util/COLOURS.js +++ b/src/react/src/util/COLOURS.js @@ -2,16 +2,45 @@ export default { // Blues NAVY_BLUE: '#0427a4', LIGHT_BLUE: '#009ee6', + MATERIAL_BLUE: '#1976d2', + LIGHT_MATERIAL_BLUE: '#1565c0', + EXTRA_LIGHT_BLUE: '#e3f2fd', + LIGHT_BLUE_BORDER: '#90caf9', + DARK_BLUE: '#0d47a1', // Greens GREEN: '#E0F5E3', DARK_GREEN: '#4A9957', MINT_GREEN: '#C0EBC7', OLIVA_GREEN: '#799679', + MATERIAL_GREEN: '#388e3c', + LIGHT_GREEN: '#e8f5e9', + LIGHT_GREEN_BORDER: '#81c784', + NOTE_GREEN: '#c8e6c9', + GREEN_TEXT: '#2e7d32', + DARK_MATERIAL_GREEN: '#1b5e20', // Reds LIGHT_RED: '#FFEAEA', RED: '#F44336', + MATERIAL_RED: '#d32f2f', + + // Purples + LIGHT_PURPLE: '#8428FA21', + PURPLE: '#8428FA', + MATERIAL_PURPLE: '#7b1fa2', + DARK_PURPLE: '#4a148c', + PURPLE_TEXT: '#6a1b9a', + LIGHT_PURPLE_BG: '#f3e5f5', + LIGHT_PURPLE_BORDER: '#ce93d8', + + // Oranges/Ambers + AMBER: '#ffc107', + DARK_AMBER: '#ffa000', + DEEP_ORANGE: '#e65100', + ORANGE: '#f57c00', + AMBER_YELLOW: '#fbc02d', + LIGHT_AMBER: '#fff8e1', // Grays and Neutrals WHITE: '#FFF', @@ -19,6 +48,10 @@ export default { LIGHT_GREY: '#F9F7F7', DARK_GREY: '#6E707E', DARK_SLATE_GREY: '#3D4153', + MEDIUM_GREY: '#666', + HOVER_GREY: '#f5f5f5', + LIGHT_BORDER: '#ccc', + MEDIUM_BORDER: '#999', NEAR_BLACK: '#0D1128', // Accent Colors @@ -26,10 +59,7 @@ export default { PALE_LIGHT_YELLOW: '#FFF2CE', ACCENT_GREY: '#E7E8EA', - // Purples - LIGHT_PURPLE: '#8428FA21', - PURPLE: '#8428FA', - // Blacks JET_BLACK: '#191919', + BLACK: '#000', }; diff --git a/src/react/src/util/constants.jsx b/src/react/src/util/constants.jsx index de3561392..26b58363c 100644 --- a/src/react/src/util/constants.jsx +++ b/src/react/src/util/constants.jsx @@ -18,6 +18,8 @@ export const MERGE_ACTION = 'merge'; export const REJECT_ACTION = 'reject'; export const InfoLink = 'https://info.opensupplyhub.org'; +export const ClaimFacilityInfoLink = + 'https://info.opensupplyhub.org/resources/claim-a-facility'; export const EMPTY_PLACEHOLDER = 'N/A'; @@ -337,6 +339,8 @@ export const facilityListItemsRoute = '/lists/:listID'; export const facilitiesRoute = '/facilities'; export const facilityDetailsRoute = '/facilities/:osID'; export const claimFacilityRoute = '/facilities/:osID/claim'; +export const claimDetailsRoute = '/claim/:osID/details'; +export const claimIntroRoute = '/claim/:osID'; export const profileRoute = '/profile/:id'; export const aboutProcessingRoute = `${InfoLink}/${InfoPaths.dataQuality}`; export const dashboardRoute = '/dashboard'; diff --git a/src/react/src/util/util.js b/src/react/src/util/util.js index 4c74b18ee..ec64b897d 100644 --- a/src/react/src/util/util.js +++ b/src/react/src/util/util.js @@ -284,6 +284,8 @@ export const makeProductionLocationURL = (osID = '') => { return `/api/v1/production-locations/${osIDPathParameter}`; }; +export const makeClaimDetailsLink = osID => `/claim/${osID}/details`; + export const makeGetProductionLocationsForPotentialMatches = ( productionLocationName, address,