From 9ab20418f277733d33f5ffb22eae49d1c684b593 Mon Sep 17 00:00:00 2001 From: aeddaqqa Date: Wed, 25 Oct 2023 00:27:36 +0100 Subject: [PATCH] feat: partners showroom section --- package.json | 5 +- src/@types/partners.ts | 148 ++++++ .../Partners/BusinessFieldFilter.tsx | 81 ++++ .../Partners/PartnerBusinessFields.tsx | 56 +++ src/components/Partners/PartnerCard.tsx | 101 ++++ src/components/Partners/PartnerFlag.tsx | 20 + src/components/Partners/PartnerLogo.tsx | 431 ++++++++++++++++++ src/components/Partners/PartnersFilter.tsx | 45 ++ src/components/Partners/SearchInput.tsx | 47 ++ src/constants/apps-consts.ts | 26 ++ src/constants/route-paths.ts | 2 + src/helpers/partnersReducer.ts | 73 +++ src/layout/MainLayout.tsx | 2 + src/layout/PartnersLayout.tsx | 40 ++ src/layout/RoutesSuite.tsx | 17 +- src/layout/SettingsLayout.tsx | 4 +- src/redux/services/partners.ts | 40 ++ src/redux/store.ts | 5 +- src/theme/index.tsx | 63 ++- src/theme/overrides/Select.ts | 14 +- src/theme/palette.ts | 46 ++ src/theme/shadows.ts | 11 +- src/theme/typography.ts | 4 +- src/views/partners/ListPartners.tsx | 44 ++ src/views/partners/Partner.tsx | 276 +++++++++++ src/views/partners/index.tsx | 95 ++++ src/views/settings/Links.tsx | 53 ++- 27 files changed, 1697 insertions(+), 52 deletions(-) create mode 100644 src/@types/partners.ts create mode 100644 src/components/Partners/BusinessFieldFilter.tsx create mode 100644 src/components/Partners/PartnerBusinessFields.tsx create mode 100644 src/components/Partners/PartnerCard.tsx create mode 100644 src/components/Partners/PartnerFlag.tsx create mode 100644 src/components/Partners/PartnerLogo.tsx create mode 100644 src/components/Partners/PartnersFilter.tsx create mode 100644 src/components/Partners/SearchInput.tsx create mode 100644 src/helpers/partnersReducer.ts create mode 100644 src/layout/PartnersLayout.tsx create mode 100644 src/redux/services/partners.ts create mode 100644 src/views/partners/ListPartners.tsx create mode 100644 src/views/partners/Partner.tsx create mode 100644 src/views/partners/index.tsx diff --git a/package.json b/package.json index 1e69e7cf..f1b76804 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "axios": "^1.1.3", "babel-loader": "^8.2.2", "copy-webpack-plugin": "^11.0.0", + "cypress": "12.5.1", "cypress-file-upload": "^4.0.7", "dotenv-webpack": "^8.0.1", "eslint": "^8.34.0", @@ -78,11 +79,11 @@ "webpack-cli": "^5.0.0", "webpack-dev-server": "^4.3.1", "webpack-merge": "^5.8.0", - "yup": "^0.32.11", - "cypress": "12.5.1" + "yup": "^0.32.11" }, "dependencies": { "react": "^18.2.0", + "react-circle-flags": "^0.0.18", "react-dom": "^18.2.0" } } diff --git a/src/@types/partners.ts b/src/@types/partners.ts new file mode 100644 index 00000000..4be89d19 --- /dev/null +++ b/src/@types/partners.ts @@ -0,0 +1,148 @@ +export interface PartnerDataType { + id?: number + attributes?: AttributesType +} + +export interface AttributesType { + contactEmail?: string + companyName?: string + companyCountry?: string + companyWebsite?: string + contactFirstname?: string + contactLastname?: string + contactPhone?: string + companyShortDescription?: string + companyLongDescription?: string + isConsortiumMember?: boolean + createdAt?: string + updatedAt?: string + publishedAt?: string + logoBox?: string + companyLinkedin?: string + companyTwitter?: string + companyLogoColor?: CompanyLogoColorType + business_fields?: BusinessFieldsType + company_size?: CompanySizeType + companyLogoDark?: CompanyLogoDarkType + companyLogoLight?: CompanyLogoLightType + country_flag?: CountryFlagType +} + +export interface CompanyLogoColorType { + data?: LogoDataType +} + +export interface LogoDataType { + id?: number + attributes?: LogoAttributesType +} + +export interface LogoAttributesType { + name?: string + alternativeText?: string + caption?: string + width?: number + height?: number + formats?: FormatsType + hash?: string + ext?: string + mime?: string + size?: number + url?: string + previewUrl?: string + provider?: string + provider_metadata?: string + createdAt?: string + updatedAt?: string +} + +export interface FormatsType { + large?: FormatDetailType + small?: FormatDetailType + medium?: FormatDetailType + thumbnail?: FormatDetailType +} + +export interface FormatDetailType { + ext?: string + url?: string + hash?: string + mime?: string + name?: string + path?: string + size?: number + width?: number + height?: number +} + +export interface BusinessFieldsType { + data?: BusinessFieldType[] +} + +export interface BusinessFieldType { + id?: number + attributes?: BusinessFieldAttributesType +} + +export interface BusinessFieldAttributesType { + BusinessField?: string + createdAt?: string + updatedAt?: string +} + +export interface CompanySizeType { + data?: CompanySizeDataType +} + +export interface CompanySizeDataType { + id?: number + attributes?: CompanySizeAttributesType +} + +export interface CompanySizeAttributesType { + companyNumberOfEmployees?: string + createdAt?: string + updatedAt?: string + publishedAt?: string +} + +export interface CompanyLogoDarkType { + data?: LogoDataType +} + +export interface CompanyLogoLightType { + data?: LogoDataType +} + +export interface CountryFlagType { + data?: CountryFlagDataType +} + +export interface CountryFlagDataType { + id?: number + attributes?: CountryFlagAttributesType +} + +export interface CountryFlagAttributesType { + countryIdentifier?: string + countryName?: string + createdAt?: string + updatedAt?: string + publishedAt?: string +} + +export interface PartnersResponseType { + data?: PartnerDataType[] + meta?: MetaType +} + +export interface MetaType { + pagination?: PaginationType +} + +export interface PaginationType { + page: number + pageSize: number + pageCount: number + total: number +} diff --git a/src/components/Partners/BusinessFieldFilter.tsx b/src/components/Partners/BusinessFieldFilter.tsx new file mode 100644 index 00000000..59c55ac1 --- /dev/null +++ b/src/components/Partners/BusinessFieldFilter.tsx @@ -0,0 +1,81 @@ +import React from 'react' + +import { mdiCheckCircle } from '@mdi/js' +import Icon from '@mdi/react' +import { Typography } from '@mui/material' +import ListItemText from '@mui/material/ListItemText' +import MenuItem from '@mui/material/MenuItem' +import Select, { SelectChangeEvent } from '@mui/material/Select' +import { ActionType, StatePartnersType, partnersActions } from '../../helpers/partnersReducer' +interface BusinessFieldFilterProps { + state: StatePartnersType + dispatchPartnersActions: React.Dispatch +} + +const BusinessFieldFilter: React.FC = ({ + state, + dispatchPartnersActions, +}) => { + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value }, + } = event + dispatchPartnersActions({ type: partnersActions.UPDATE_BUSINESS_FIELD, payload: value[1] }) + } + + return ( + + ) +} + +export default BusinessFieldFilter diff --git a/src/components/Partners/PartnerBusinessFields.tsx b/src/components/Partners/PartnerBusinessFields.tsx new file mode 100644 index 00000000..5bbabca4 --- /dev/null +++ b/src/components/Partners/PartnerBusinessFields.tsx @@ -0,0 +1,56 @@ +import { Box, Chip } from '@mui/material' +import React from 'react' + +type PartnerBusinessFieldsProps = { business_fields: any; isPartnerView?: boolean } + +const PartnerBusinessFields = ({ business_fields, isPartnerView }: PartnerBusinessFieldsProps) => { + const content = + business_fields.data.length <= 2 || isPartnerView ? ( + business_fields.data.map((elem, key) => ( + theme.palette.grey['700'], + }} + label={elem.attributes.BusinessField} + /> + )) + ) : ( + <> + theme.palette.grey['700'], + }} + label={business_fields.data[0].attributes.BusinessField} + /> + theme.palette.grey['700'], + }} + label={`+${business_fields.data.length - 1}`} + /> + + ) + return ( + + {content} + + ) +} + +export default PartnerBusinessFields diff --git a/src/components/Partners/PartnerCard.tsx b/src/components/Partners/PartnerCard.tsx new file mode 100644 index 00000000..c000b8f5 --- /dev/null +++ b/src/components/Partners/PartnerCard.tsx @@ -0,0 +1,101 @@ +import { Box, Typography } from '@mui/material' +import React from 'react' +import { PartnerDataType } from '../../@types/partners' +import PartnerBusinessFields from './PartnerBusinessFields' +import PartnerFlag from './PartnerFlag' +import PartnerLogo from './PartnerLogo' + +interface PartnerCardProps { + onClick: () => void + partner: PartnerDataType + index: number +} + +const PartnerCard: React.FC = ({ partner, onClick }) => { + const { + attributes: { + isConsortiumMember, + companyName, + companyShortDescription, + business_fields, + companyLogoColor, + country_flag, + logoBox, + }, + } = partner + + return ( + theme.palette.card.background, + border: theme => `2px solid ${theme.palette.card.border}`, + borderRadius: '16px', + padding: '24px', + display: 'flex', + flexDirection: 'column', + gap: '12px', + position: 'relative', + overflow: 'hidden', + }} + > + {/* + // this value is incorrect, it should be based on the pchain addres but its not yet added to the api + {!!isConsortiumMember && ( + theme.palette.background.gradient, + padding: '10px 14px 8px 12px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderBottomLeftRadius: '8px', + right: '0', + top: '0', + }} + > + Validator + + )} */} + {!!companyLogoColor && !!companyName && ( + + )} + + {!!companyName && {companyName}} + + + {!!companyShortDescription && ( + theme.palette.card.text, + }} + > + {companyShortDescription} + + )} + + {!!country_flag && !!country_flag.data?.attributes && ( + + )} + {!!business_fields && } + + ) +} + +export default PartnerCard diff --git a/src/components/Partners/PartnerFlag.tsx b/src/components/Partners/PartnerFlag.tsx new file mode 100644 index 00000000..0512abf8 --- /dev/null +++ b/src/components/Partners/PartnerFlag.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { Box, Typography } from '@mui/material' +import { CircleFlag } from 'react-circle-flags' +import { CountryFlagAttributesType } from '../../@types/partners' + +interface PartnerFlagProps { + country: CountryFlagAttributesType +} + +const PartnerFlag: React.FC = ({ + country: { countryName, countryIdentifier }, +}) => { + return ( + + + {countryName} + + ) +} +export default React.memo(PartnerFlag) diff --git a/src/components/Partners/PartnerLogo.tsx b/src/components/Partners/PartnerLogo.tsx new file mode 100644 index 00000000..9354eaba --- /dev/null +++ b/src/components/Partners/PartnerLogo.tsx @@ -0,0 +1,431 @@ +import { Box } from '@mui/material' +import React from 'react' +import { CompanyLogoColorType } from '../../@types/partners' +import { CAMINO_STRAPI } from '../../constants/route-paths' + +interface PartnerLogoProps { + companyName: string + colorLogo: CompanyLogoColorType + logoBox: string +} + +const partners = [ + { + partner: '51 nodes', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'A3M Global Monitoring', + logoBox: 'noLogoBox', + }, + { + partner: 'AERTiCKET', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Algoteque', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Alpitour', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Andersen Group', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Asian Trails', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'AVRA Tours', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Axis Data', + }, + { + partner: 'Bentour', + }, + { + partner: 'Beereal', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Berge & Meer Touristik', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Beyond Bookings', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Biztribution', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Blaize', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'bloXmove', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Brickken', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Buk Technology', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Busy Rooms', + }, + { + partner: 'BytePitch - Software Labs', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Camino Network Foundation', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'CENAPro', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'CFM Media', + }, + { + partner: 'Clairvoyant Lab', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Codora', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Coherent', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'CompecTA', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'DataArt', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Dfns', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Disruptive Elements', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Dreamtime Travel', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Dtravel', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Econfirm', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Ekoios', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Embarking on Voyage', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Ennea', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Enwoke', + }, + { + partner: 'Eurowings', + }, + { + partner: 'Eurowings Holidays', + }, + { + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Feratel', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Fireblocks', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Fitbooktravel', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Fraport', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Freshcells', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'FTI', + logoBox: 'noLogoBox', + }, + { + partner: 'GIATA', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'GP Solutions', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Halfmage', + missingInformation: '', + }, + { + partner: 'Hexens', + }, + { + partner: 'Hiberus', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'hlx', + }, + { + partner: 'Holiday Pirates', + }, + { + partner: 'Startseite', + }, + { + partner: 'Huerzeler', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Immutable Insight', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'InfoComm Management sarl', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Invia', + }, + { + partner: 'IT Kompass', + }, + { + partner: 'ITsquare', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Joonze', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Juniper Travel Technology', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Lufthansa', + }, + { + partner: 'Lufthansa City Center', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Lufthansa Holidays', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'MC2 Ventures', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'meinReisebüro24', + }, + { + partner: 'Midoco Group', + }, + { + partner: 'Miles & More', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'MTS', + logoBox: '', + }, + { + partner: 'Netactica', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Nevermined', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'OH.TEC Tourism Expert Consulting', + }, + { + partner: 'Orchestra', + }, + { + partner: 'PRODYNA', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Riversoft Inc.', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Saltours', + }, + { + partner: 'Schauinsland Reisen', + }, + { + partner: 'Schmetterling', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'SH Financial', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Sixt', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Stripe', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Studio 5', + }, + { + partner: 'Sugartrends', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Sunweb', + }, + { + partner: 'TenetX', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Traffics', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'TraSo', + logoBox: 'noLogoBox', + }, + { + partner: 'Travel Ledger', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Triend', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Turismoi', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Unicsoft', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Universal Beach Hotels', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Universal Mallorca Ferien', + }, + { + partner: 'Ventura Travel', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Vibe', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Videvago', + }, + { + partner: 'Vidma Security', + logoBox: 'darkBgLogoBox', + }, + { + partner: 'Vivin Software Private Limited', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Whispers', + logoBox: 'brightBgLogoBox', + }, + { + partner: 'Xeni', + }, +] +const determineBackground = (logoBox: string, companyName: string) => { + let partner = partners.find(partner => partner.partner === companyName) + let logoBoxBg = partner?.logoBox ? partner.logoBox : logoBox + switch (logoBoxBg) { + case 'darkBgLogoBox': + return 'black' + case 'noLogoBox': + return 'transparent' + case 'brightBgLogoBox': + return 'white' + default: + return 'transparent' + } +} +const PartnerLogo: React.FC = ({ companyName, colorLogo, logoBox }) => { + return ( + + {colorLogo?.data?.attributes?.url && ( + {`${companyName} + )} + + ) +} + +export default React.memo(PartnerLogo) diff --git a/src/components/Partners/PartnersFilter.tsx b/src/components/Partners/PartnersFilter.tsx new file mode 100644 index 00000000..566e092a --- /dev/null +++ b/src/components/Partners/PartnersFilter.tsx @@ -0,0 +1,45 @@ +import { Box, Checkbox, FormControlLabel } from '@mui/material' +import React from 'react' +import { ActionType, StatePartnersType, partnersActions } from '../../helpers/partnersReducer' +import BusinessFieldFilter from './BusinessFieldFilter' +import SearchInput from './SearchInput' + +interface PartnersFilterProps { + state: StatePartnersType + dispatchPartnersActions: React.Dispatch +} + +const PartnersFilter: React.FC = ({ state, dispatchPartnersActions }) => { + const searchByName = param => + dispatchPartnersActions({ type: partnersActions.UPDATE_COMPANY_NAME, payload: param }) + return ( + + + + + theme.palette.secondary.main, + '&.Mui-checked': { + color: theme => theme.palette.secondary.main, + }, + '&.MuiCheckbox-colorSecondary.Mui-checked': { + color: theme => theme.palette.secondary.main, + }, + }} + checked={state.validators} + onChange={() => + dispatchPartnersActions({ type: partnersActions.TOGGLE_VALIDATORS }) + } + /> + } + /> + + + ) +} + +export default React.memo(PartnersFilter) diff --git a/src/components/Partners/SearchInput.tsx b/src/components/Partners/SearchInput.tsx new file mode 100644 index 00000000..9cad684f --- /dev/null +++ b/src/components/Partners/SearchInput.tsx @@ -0,0 +1,47 @@ +import { Box, InputAdornment, OutlinedInput } from '@mui/material' +import React, { useCallback } from 'react' +import SearchIcon from '@mui/icons-material/Search' +import { debounce } from 'lodash' + +const SearchInput = ({ searchByName }) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedSearchByName = useCallback( + debounce(searchByName, 300), + [], // will only create this function once on mount + ) + return ( + + `solid 2px ${theme.palette.card.border}`, + borderRadius: '12px', + fontSize: '15px', + lineHeight: '24px', + fontWeight: 500, + '.MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + // backgroundColor: '#475569', + }} + startAdornment={ + + + + } + onChange={e => debouncedSearchByName(e.target.value)} + /> + + ) +} + +export default SearchInput diff --git a/src/constants/apps-consts.ts b/src/constants/apps-consts.ts index b108ea39..1416c4b0 100644 --- a/src/constants/apps-consts.ts +++ b/src/constants/apps-consts.ts @@ -18,6 +18,12 @@ export const APPS_CONSTS = [ url: '/explorer', private: false, }, + { + name: 'Partners', + subText: 'Partners of the Camino Network.', + url: '/partners', + private: false, + }, { name: 'Settings', subText: 'Lookup network activity and statistics.', @@ -30,3 +36,23 @@ export const APPS_CONSTS = [ export const TIMEOUT_DURATION = 60000 * 20 // in milliseconde export const DRAWER_WIDTH = 300 + +export const BUSINESS_FIELDS = [ + 'Aerospace', + 'Customer Engagement', + 'Loyalty', + 'Transportation', + 'Reviews', + 'Consulting', + 'Data Insights', + 'Distribution', + 'Finance', + 'Hospitality', + 'Software Development', + 'Travel Technology', + 'E-Mobility', + 'Security', + 'Software as a Service', + 'Metaverse', + 'Climate Technology', +] diff --git a/src/constants/route-paths.ts b/src/constants/route-paths.ts index b7a1280f..5e39fd1d 100644 --- a/src/constants/route-paths.ts +++ b/src/constants/route-paths.ts @@ -14,6 +14,8 @@ export const GITHUB = 'https://github.com/chain4travel/' export const CAMINO = 'https://camino.network/' +export const CAMINO_STRAPI = 'https://api.strapi.camino.network/' + export const SUITE_RELEASES = 'https://github.com/chain4travel/camino-suite/releases' export const SUITE_WALLET = 'https://github.com/chain4travel/camino-wallet/tree/suite' diff --git a/src/helpers/partnersReducer.ts b/src/helpers/partnersReducer.ts new file mode 100644 index 00000000..c3496c33 --- /dev/null +++ b/src/helpers/partnersReducer.ts @@ -0,0 +1,73 @@ +import { BUSINESS_FIELDS } from '../constants/apps-consts' + +export interface BusinessField { + name: string + active: boolean +} + +export interface StatePartnersType { + page: number + companyName: string + businessField: BusinessField[] + validators: boolean +} + +export const initialStatePartners: StatePartnersType = { + page: 1, + companyName: '', + businessField: BUSINESS_FIELDS.map(elem => { + return { name: elem, active: false } + }), + validators: false, +} + +export enum partnersActions { + 'NEXT_PAGE', + 'UPDATE_COMPANY_NAME', + 'UPDATE_BUSINESS_FIELD', + 'TOGGLE_VALIDATORS', +} + +export interface ActionType { + type: partnersActions + payload?: any +} + +export const partnersReducer = ( + state: StatePartnersType, + action: ActionType, +): StatePartnersType => { + switch (action.type) { + case partnersActions.NEXT_PAGE: + return { + ...state, + page: action.payload, + } + case partnersActions.UPDATE_COMPANY_NAME: + return { + ...state, + page: 1, + companyName: action.payload, + } + case partnersActions.UPDATE_BUSINESS_FIELD: + let newBusinessField = [...state.businessField] + let index = newBusinessField.findIndex(elem => elem.name === action.payload) + newBusinessField[index] = { + ...newBusinessField[index], + active: !newBusinessField[index].active, + } + return { + ...state, + page: 1, + businessField: newBusinessField, + } + case partnersActions.TOGGLE_VALIDATORS: + return { + ...state, + page: 1, + validators: !state.validators, + } + default: + return state + } +} diff --git a/src/layout/MainLayout.tsx b/src/layout/MainLayout.tsx index 58b007bf..c9a49d0b 100644 --- a/src/layout/MainLayout.tsx +++ b/src/layout/MainLayout.tsx @@ -23,6 +23,8 @@ const MainLayout = ({ children }) => { if (location.pathname.split('/')[1] === 'wallet') dispatch(changeActiveApp('Wallet')) else if (location.pathname.split('/')[1] === 'explorer') dispatch(changeActiveApp('Explorer')) + else if (location.pathname.split('/')[1] === 'partners') + dispatch(changeActiveApp('Partners')) dispatch(changeNetworkStatus(Status.LOADING)) await store.dispatch('Network/init') diff --git a/src/layout/PartnersLayout.tsx b/src/layout/PartnersLayout.tsx new file mode 100644 index 00000000..ea937e07 --- /dev/null +++ b/src/layout/PartnersLayout.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Outlet } from 'react-router' +import { Box, Toolbar } from '@mui/material' +import Links from '../views/settings/Links' +const PartnersLayout = () => { + return ( + + theme.palette.background.paper, + flexGrow: 1, + px: '0rem !important', + zIndex: 9, + position: 'fixed', + top: '65px', + width: '100vw', + height: '61px', + display: 'flex', + justifyContent: 'center', + right: 0, + }} + > + + + + + ) +} + +export default PartnersLayout diff --git a/src/layout/RoutesSuite.tsx b/src/layout/RoutesSuite.tsx index 4ca54c5f..2196933d 100644 --- a/src/layout/RoutesSuite.tsx +++ b/src/layout/RoutesSuite.tsx @@ -1,20 +1,22 @@ -import { Navigate, Route, Routes, useNavigate } from 'react-router-dom' import React, { useEffect, useState } from 'react' +import { Navigate, Route, Routes, useNavigate } from 'react-router-dom' +import { useAppSelector } from '../hooks/reduxHooks' +import { getActiveNetwork } from '../redux/slices/network' import AccessLayout from '../views/access' +import MountAccessComponent from '../views/access/MountAccessComponent' import Create from '../views/create/Create' import ExplorerApp from '../views/explorer/ExplorerApp' import LandingPage from '../views/landing/LandingPage' import Legal from '../views/legal/Legal' import LoginPage from '../views/login/LoginPage' -import MountAccessComponent from '../views/access/MountAccessComponent' +import Partners from '../views/partners' import MultisigWallet from '../views/settings/MultisigWallet' -import Protected from './Protected' import Settings from '../views/settings/index' -import SettingsLayout from './SettingsLayout' import Wallet from '../views/wallet/WalletApp' -import { getActiveNetwork } from '../redux/slices/network' -import { useAppSelector } from '../hooks/reduxHooks' +import PartnersLayout from './PartnersLayout' +import Protected from './Protected' +import SettingsLayout from './SettingsLayout' export default function RoutesSuite() { const navigate = useNavigate() @@ -78,6 +80,9 @@ export default function RoutesSuite() { } /> + }> + } /> + } /> } /> } /> diff --git a/src/layout/SettingsLayout.tsx b/src/layout/SettingsLayout.tsx index 60c6c948..71c55038 100644 --- a/src/layout/SettingsLayout.tsx +++ b/src/layout/SettingsLayout.tsx @@ -1,6 +1,6 @@ +import { Box, Toolbar } from '@mui/material' import React from 'react' import { Outlet } from 'react-router' -import { Box, Toolbar } from '@mui/material' import Links from '../views/settings/Links' const SettingsLayout = () => { return ( @@ -30,7 +30,7 @@ const SettingsLayout = () => { right: 0, }} > - + diff --git a/src/redux/services/partners.ts b/src/redux/services/partners.ts new file mode 100644 index 00000000..ec860c93 --- /dev/null +++ b/src/redux/services/partners.ts @@ -0,0 +1,40 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { PartnersResponseType } from '../../@types/partners' +import { StatePartnersType } from '../../helpers/partnersReducer' + +const baseUrl = 'https://api.strapi.camino.network/partners' + +export const partnersApi = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: baseUrl }), + endpoints: build => ({ + listPartners: build.query({ + query: ({ page, companyName, businessField, validators }) => { + let query = '?populate=*' + + if (!isNaN(page)) { + query += `&sort[0]=companyName:asc&pagination[page]=${page}&pagination[pageSize]=12` + } + if (businessField) { + let filterWith = businessField + .filter(elem => elem.active) + .map(elem => elem.name) + if (filterWith?.length > 0) { + filterWith.forEach((element, index) => { + query += `&filters[$and][${index}][business_fields][BusinessField][$eq]=${element}` + }) + } + } + if (companyName) { + query += `&filters[companyName][$contains]=${companyName}` + } + if (validators) { + query += `&filters[isConsortiumMember][$eq]=true` + } + + return query + }, + }), + }), +}) + +export const { useListPartnersQuery } = partnersApi diff --git a/src/redux/store.ts b/src/redux/store.ts index b4d1e49a..421f2315 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -2,7 +2,7 @@ import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit' import appConfigReducer from './slices/app-config' import themeReducer from './slices/theme' import network from './slices/network' - +import { partnersApi } from './services/partners' export type AppDispatch = typeof store.dispatch export type RootState = ReturnType export type AppThunk = ThunkAction< @@ -18,11 +18,12 @@ export function configureAppStore() { appConfig: appConfigReducer, theme: themeReducer, network: network, + [partnersApi.reducerPath]: partnersApi.reducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false, - }), + }).concat(partnersApi.middleware), }) return store } diff --git a/src/theme/index.tsx b/src/theme/index.tsx index a686f73f..682551b8 100644 --- a/src/theme/index.tsx +++ b/src/theme/index.tsx @@ -1,25 +1,61 @@ -import React from 'react' -import { useMemo, ReactNode } from 'react' import { CssBaseline } from '@mui/material' import { - createTheme, + StyledEngineProvider, ThemeOptions, ThemeProvider, - StyledEngineProvider, + createTheme, } from '@mui/material/styles' -import shape from './shape' -import palette from './palette' -import typography from './typography' -import breakpoints from './breakpoints' -import componentsOverrides from './overrides' +import React, { ReactNode, useMemo } from 'react' import { useAppSelector } from '../hooks/reduxHooks' import { getTheme } from '../redux/slices/theme' -import shadow, { customShadows } from './shadows' +import breakpoints from './breakpoints' +import componentsOverrides from './overrides' +import palette from './palette' +import shadow, { CustomShadowOptions, customShadows } from './shadows' +import shape from './shape' +import typography from './typography' type ThemeConfigProps = { children: ReactNode } +interface CustomPaddingOptions { + defaultPadding: string +} +interface CustomWidthOptions { + layoutMaxWitdh: string +} +interface CustomShapeOptions { + borderRadiusNone: number | string + borderRadiusSm: number | string + borderRadiusMd: number | string + borderRadiusLg: number | string + borderRadiusXl: number | string +} + +const customShape = { + borderRadiusNone: 0, + borderRadius: 8, + borderRadiusSm: 12, + borderRadiusMd: 16, + borderRadiusLg: 20, + borderRadiusXl: 25, +} +declare module '@mui/material/styles' { + interface Theme { + customShadows: CustomShadowOptions + customPadding: CustomPaddingOptions + customWidth: CustomWidthOptions + customShape: CustomShapeOptions + } + interface ThemeOptions { + customShadows?: CustomShadowOptions + customPadding?: CustomPaddingOptions + customWidth?: CustomWidthOptions + customShape?: CustomShapeOptions + } +} + export default function ThemeConfig({ children }: ThemeConfigProps) { const mode = useAppSelector(getTheme) const isLight = mode === 'light' @@ -34,6 +70,13 @@ export default function ThemeConfig({ children }: ThemeConfigProps) { typography, breakpoints, customShadows: isLight ? customShadows.light : customShadows.dark, + customPadding: { + defaultPadding: '10px 16px 10px 16px', + }, + customWidth: { + layoutMaxWitdh: '1536px', + }, + customShape, }), [isLight], ) diff --git a/src/theme/overrides/Select.ts b/src/theme/overrides/Select.ts index 4c6cbaf7..104a8558 100644 --- a/src/theme/overrides/Select.ts +++ b/src/theme/overrides/Select.ts @@ -1,5 +1,5 @@ -import { Theme } from '@mui/material/styles' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { Theme } from '@mui/material/styles' export default function Select(theme: Theme) { return { @@ -8,6 +8,11 @@ export default function Select(theme: Theme) { IconComponent: ExpandMoreIcon, }, styleOverrides: { + root: { + '& .MuiSvgIcon-root': { + fill: theme.palette.primary.light, + }, + }, select: { padding: '1rem', paddingLeft: '.5rem', @@ -15,5 +20,12 @@ export default function Select(theme: Theme) { }, }, }, + MuiMenu: { + styleOverrides: { + list: { + maxWidth: 'none !important', + }, + }, + }, } } diff --git a/src/theme/palette.ts b/src/theme/palette.ts index aef2e522..4a38b839 100644 --- a/src/theme/palette.ts +++ b/src/theme/palette.ts @@ -1,14 +1,41 @@ import { alpha } from '@mui/material/styles' +interface PaletteColorCustomOptions { + primary?: string + secondary?: string + border?: string + background?: string + text?: string +} declare module '@mui/material/styles/createPalette' { interface ColorTypes { primary: string } interface Palette { logo: ColorTypes + card: PaletteColorCustomOptions + blue: { + [key in + | '0' + | '50' + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900']: string + } + } + interface PaletteOptions { + logo?: PaletteColorCustomOptions + card?: PaletteColorCustomOptions } interface TypeBackground { secondary: string + gradient: string } } @@ -127,10 +154,20 @@ const palette = { default: GREY[200], neutral: GREY[200], secondary: GREY[0], + gradient: 'linear-gradient(to right, #0085FF, #B440FC)', }, action: { active: GREY[600], ...COMMON.action }, button: { primary: GREY[200], secondary: GREY[100] }, logo: { primary: BLUE[400] }, + border: { + infoCard: '#B5E3FD', + }, + card: { + border: '#E2E7F0', + borderSecondary: '#334155', + background: '#FFFFFF', + text: '#334155', + }, }, dark: { ...COMMON, @@ -140,10 +177,19 @@ const palette = { default: GREY[900], neutral: GREY[500_16], secondary: GREY[800], + gradient: 'linear-gradient(to right, #0085FF, #B440FC)', + }, + border: { + infoCard: '#B5E3FD', }, action: { active: GREY[500], ...COMMON.action }, button: { primary: GREY[700], secondary: GREY[800] }, logo: { primary: BLUE[50] }, + card: { + border: '#334155', + borderSecondary: '#E2E7F0', + text: '#CBD4E2', + }, }, } diff --git a/src/theme/shadows.ts b/src/theme/shadows.ts index 999847ef..7e7bb949 100644 --- a/src/theme/shadows.ts +++ b/src/theme/shadows.ts @@ -2,7 +2,7 @@ import { alpha } from '@mui/material/styles' import { Shadows } from '@mui/material/styles/shadows' import palette from './palette' -interface CustomShadowOptions { +export interface CustomShadowOptions { z1: string z8: string z12: string @@ -17,15 +17,6 @@ interface CustomShadowOptions { error: string } -declare module '@mui/material/styles' { - interface Theme { - customShadows: CustomShadowOptions - } - interface ThemeOptions { - customShadows?: CustomShadowOptions - } -} - const LIGHT_MODE = palette.light.grey[200] const DARK_MODE = '#424242' diff --git a/src/theme/typography.ts b/src/theme/typography.ts index d4539656..e92e7d66 100644 --- a/src/theme/typography.ts +++ b/src/theme/typography.ts @@ -31,7 +31,7 @@ const typography = { fontWeight: 700, lineHeight: 80 / 64, fontSize: pxToRem(40), - ...responsiveFontSizes({ sm: 52, md: 58, lg: 64 }), + // ...responsiveFontSizes({ sm: 52, md: 58, lg: 64 }), }, h2: { fontFamily: FONT_PRIMARY, @@ -45,7 +45,7 @@ const typography = { fontWeight: 700, lineHeight: 1.5, fontSize: pxToRem(24), - ...responsiveFontSizes({ sm: 26, md: 30, lg: 32 }), + // ...responsiveFontSizes({ sm: 26, md: 30, lg: 32 }), }, h4: { fontFamily: FONT_PRIMARY, diff --git a/src/views/partners/ListPartners.tsx b/src/views/partners/ListPartners.tsx new file mode 100644 index 00000000..29766d76 --- /dev/null +++ b/src/views/partners/ListPartners.tsx @@ -0,0 +1,44 @@ +import { Box } from '@mui/material' +import React from 'react' +import { PartnerDataType, PartnersResponseType } from '../../@types/partners' +import PartnerCard from '../../components/Partners/PartnerCard' + +interface ListPartnersProps { + partners: PartnersResponseType + setPartner: React.Dispatch> +} + +const ListPartners: React.FC = ({ partners, setPartner }) => { + return ( + theme.customWidth.layoutMaxWitdh, + }} + > + {partners.data.map((partner, index) => ( + { + if ( + partner.attributes.companyName && + partner.attributes.companyLongDescription && + partner.attributes.companyWebsite && + partner.attributes.contactEmail && + partner.attributes.contactFirstname && + partner.attributes.contactLastname && + partner.attributes.contactPhone + ) + setPartner(partner) + }} + partner={partner} + key={index} + index={index} + /> + ))} + + ) +} + +export default React.memo(ListPartners) diff --git a/src/views/partners/Partner.tsx b/src/views/partners/Partner.tsx new file mode 100644 index 00000000..44cd87ab --- /dev/null +++ b/src/views/partners/Partner.tsx @@ -0,0 +1,276 @@ +import { mdiArrowLeft } from '@mdi/js' +import Icon from '@mdi/react' +import { Box, Button, Divider, Typography, useTheme } from '@mui/material' +import React from 'react' +import { Link } from 'react-router-dom' +import { PartnerDataType } from '../../@types/partners' +import PartnerBusinessFields from '../../components/Partners/PartnerBusinessFields' +import PartnerFlag from '../../components/Partners/PartnerFlag' +import PartnerLogo from '../../components/Partners/PartnerLogo' + +interface PartnerProps { + partner: PartnerDataType + setPartner: React.Dispatch> +} +const Partner: React.FC = ({ partner, setPartner }) => { + const { + attributes: { + isConsortiumMember, + companyName, + companyShortDescription, + companyLongDescription, + business_fields, + companyLogoColor, + country_flag, + contactEmail, + companyWebsite, + contactPhone, + contactFirstname, + contactLastname, + logoBox, + }, + } = partner + const theme = useTheme() + const isDark = theme.palette.mode === 'dark' + return ( + + + + + + + + {companyName} + {!!isConsortiumMember && ( + theme.palette.background.gradient, + padding: '10px 14px 8px 12px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderRadius: theme => theme.shape.borderRadius, + width: 'fit-content', + }} + > + Validator + + )} + + {companyShortDescription} + + + + + Description + {companyLongDescription} + + + `2px solid ${theme.palette.blue[50]}`, + borderRadius: '16px', + }, + !isDark && { backgroundColor: theme => theme.palette.background.paper }, + ]} + > + + + + + + theme.palette.text.primary, + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 600, + lineHeight: '20px', + textTransform: 'uppercase', + }} + > + company country + + {country_flag && country_flag.data?.attributes && ( + + )} + + + + theme.palette.text.primary, + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 600, + lineHeight: '20px', + textTransform: 'uppercase', + }} + > + Direct Contact + + theme.palette.card.text, + fontSize: '16px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '150%', + }} + > + {contactFirstname + ' ' + contactLastname} + + + + + theme.palette.text.primary, + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 600, + lineHeight: '20px', + textTransform: 'uppercase', + }} + > + Contact Email + + + theme.palette.card.text, + fontSize: '16px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '150%', + }} + > + {contactEmail} + + + + + + theme.palette.text.primary, + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 600, + lineHeight: '20px', + textTransform: 'uppercase', + }} + > + Contact Phone + + + theme.palette.card.text, + fontSize: '16px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '150%', + }} + > + {contactPhone} + + + + + + theme.palette.text.primary, + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 600, + lineHeight: '20px', + textTransform: 'uppercase', + }} + > + Website + + + theme.palette.card.text, + fontSize: '16px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '150%', + }} + > + {companyWebsite} + + + + + + + ) +} + +export default Partner diff --git a/src/views/partners/index.tsx b/src/views/partners/index.tsx new file mode 100644 index 00000000..08d924d2 --- /dev/null +++ b/src/views/partners/index.tsx @@ -0,0 +1,95 @@ +import React, { ReactNode, useReducer, useState } from 'react' +import { Box, CircularProgress, Pagination, PaginationItem, Typography } from '@mui/material' +import { useListPartnersQuery } from '../../redux/services/partners' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' +import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import ListPartners from './ListPartners' +import PartnersFilter from '../../components/Partners/PartnersFilter' +import { + initialStatePartners, + partnersActions, + partnersReducer, +} from '../../helpers/partnersReducer' +import Partner from './Partner' +import { PartnerDataType } from '../../@types/partners' + +interface PartnersListWrapperProps { + isLoading: boolean + isFetching: boolean + children?: ReactNode +} + +const PartnersListWrraper: React.FC = ({ + isLoading, + isFetching, + children, +}) => { + if (isLoading || isFetching) + return ( + + + + ) + return <>{children} +} + +const Partners = () => { + const [state, dispatchPartnersActions] = useReducer(partnersReducer, initialStatePartners) + const { data: partners, isLoading, isFetching } = useListPartnersQuery(state) + const handleChange = (event: React.ChangeEvent, value: number) => { + dispatchPartnersActions({ type: partnersActions.NEXT_PAGE, payload: value }) + } + const [partner, setPartner] = useState(null) + + if (!partners?.data) { + return + } + const content = partner ? ( + + ) : ( + <> + + {partners.meta.pagination.total} Partners + + + + + ( + + )} + /> + + + ) + return ( + theme.customWidth.layoutMaxWitdh, + }} + > + {content} + + ) +} + +export default Partners diff --git a/src/views/settings/Links.tsx b/src/views/settings/Links.tsx index 9bea9591..9d8e699a 100644 --- a/src/views/settings/Links.tsx +++ b/src/views/settings/Links.tsx @@ -4,8 +4,8 @@ import Box from '@mui/material/Box' import Tab from '@mui/material/Tab' import Tabs from '@mui/material/Tabs' import { useNavigate } from 'react-router' -import { changeActiveApp } from '../../redux/slices/app-config' import { useAppDispatch } from '../../hooks/reduxHooks' +import { changeActiveApp } from '../../redux/slices/app-config' function a11yProps(index: number) { return { @@ -27,6 +27,38 @@ export default function Links() { dispatch(changeActiveApp('Network')) }, [path]) // eslint-disable-line react-hooks/exhaustive-deps + const settingsTabs = [ + navigate('/settings')} + {...a11yProps(0)} + key={0} + sx={{ '&::after': { display: value === 0 ? 'block' : 'none' } }} + />, + navigate('manage-multisig')} + {...a11yProps(1)} + key={1} + sx={{ '&::after': { display: value === 1 ? 'block' : 'none' } }} + />, + ] + + const partnersTabs = [ + , + ] + return ( - navigate('/settings')} - {...a11yProps(0)} - sx={{ '&::after': { display: value === 0 ? 'block' : 'none' } }} - /> - navigate('manage-multisig')} - {...a11yProps(1)} - sx={{ '&::after': { display: value === 1 ? 'block' : 'none' } }} - /> + {path === '/partners' + ? partnersTabs.map((tab, index) => (tab ? tab : null)) + : settingsTabs.map((tab, index) => (tab ? tab : null))} )