diff --git a/package.json b/package.json index c61763532..95606ab19 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "safe-homepage", "homepage": "https://github.com/safe-global/safe-homepage", - "version": "1.4.41", + "version": "1.4.42", "scripts": { "build": "next build && next export", "lint": "next lint", diff --git a/public/images/Header/safe-foundry-icon.svg b/public/images/Header/safe-foundry-icon.svg new file mode 100644 index 000000000..988c08647 --- /dev/null +++ b/public/images/Header/safe-foundry-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/safe-foundry-logo.png b/public/images/safe-foundry-logo.png new file mode 100644 index 000000000..ddc7a5d68 Binary files /dev/null and b/public/images/safe-foundry-logo.png differ diff --git a/src/components/Foundry/Hero/index.tsx b/src/components/Foundry/Hero/index.tsx new file mode 100644 index 000000000..7afc75985 --- /dev/null +++ b/src/components/Foundry/Hero/index.tsx @@ -0,0 +1,53 @@ +import { type BaseBlockEntry } from '@/config/types' +import RichText from '@/components/common/RichText' +import ButtonsWrapper from '@/components/Token/ButtonsWrapper' +import { isAsset, isEntryTypeButton } from '@/lib/typeGuards' +import { Container, Typography } from '@mui/material' +import { useIsMediumScreen } from '@/hooks/useMaxWidth' +import css from './styles.module.css' + +const Hero = (props: BaseBlockEntry) => { + const isMediumScreen = useIsMediumScreen() + const { title, text, buttons, image, bgImage } = props.fields + + const buttonsList = buttons?.filter(isEntryTypeButton) || [] + + const imageURL = isAsset(image) && image.fields.file?.url ? image.fields.file.url : '' + const bgImageURL = isAsset(bgImage) && bgImage.fields.file?.url ? bgImage.fields.file.url : '' + + return ( +
+
+
+
+ + {/* Networks image does not show in smaller resolutions */} +
+ + Safe Foundry logo + +
+ + + + + {text && ( +
+ +
+ )} + + {buttonsList.length > 0 && ( +
+ +
+ )} +
+
+
+
+
+ ) +} + +export default Hero diff --git a/src/components/Foundry/Hero/styles.module.css b/src/components/Foundry/Hero/styles.module.css new file mode 100644 index 000000000..0946db2bc --- /dev/null +++ b/src/components/Foundry/Hero/styles.module.css @@ -0,0 +1,118 @@ +.bg { + background-position-x: center; + background-repeat: no-repeat; + overflow: visible; + background-size: cover; + position: relative; +} + +.spot1 { + position: absolute; + left: -300px; + top: 200px; + width: 600px; + height: 600px; + background-image: radial-gradient(at top left, rgba(18, 255, 128, 0.4) 0%, rgba(246, 247, 248, 0) 80%); + filter: blur(70px); +} + +.container { + margin-top: 70px; + display: flex; + flex-direction: column; + align-items: center; +} + +.textBlock { + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; +} + +.title { + max-width: 1170px; + text-align: center; +} + +.title p, +.text p { + margin: 0; +} + +.logo { + height: 32px; + margin-bottom: 16px; +} + +@media (min-width: 600px) { + .container { + margin-top: 150px; + } +} + +@media (min-width: 900px) { + .spot1 { + left: -100px; + top: 100px; + width: 700px; + height: 700px; + } + + .spot2 { + position: absolute; + right: 0px; + top: 50px; + width: 800px; + height: 800px; + background-image: radial-gradient(at right, rgba(41, 182, 246, 0.4) 0%, rgba(246, 247, 248, 0) 80%); + filter: blur(50px); + } + + .image { + background-repeat: no-repeat; + overflow: hidden; + background-size: contain; + background-position: center top 140px; + position: relative; + z-index: 1; + } + + .gradientHorizontal:before, + .gradientHorizontal:after { + content: ''; + top: 0; + display: block; + height: 100%; + position: absolute; + width: 30px; + pointer-events: none; + } + + .gradientHorizontal:before { + background: linear-gradient(-90deg, rgba(18, 19, 18, 0) 0%, rgb(20, 20, 20) 100%); + } + + .gradientHorizontal:after { + width: 1px; + background: linear-gradient(270deg, rgba(18, 19, 18, 0.1) 0%, rgba(18, 19, 18, 0) 100%); + right: 0px; + } + + .text { + text-align: center; + max-width: 780px; + } + + .logo { + height: 40px; + } +} + +@media (min-width: 1536px) { + .spot1, + .spot2 { + width: 1000px; + height: 1000px; + } +} diff --git a/src/components/Foundry/POCs/Card.tsx b/src/components/Foundry/POCs/Card.tsx new file mode 100644 index 000000000..2a8105cb4 --- /dev/null +++ b/src/components/Foundry/POCs/Card.tsx @@ -0,0 +1,46 @@ +import { Typography } from '@mui/material' +import RichText from '@/components/common/RichText' +import ButtonsWrapper from '@/components/Token/ButtonsWrapper' +import { isAsset, isEntryTypeButton } from '@/lib/typeGuards' +import { type BaseBlockEntry } from '@/config/types' +import GithubIcon from '@/public/images/github-icon.svg' +import css from './styles.module.css' + +const Card = (props: BaseBlockEntry) => { + const { caption, title, text, link, image, buttons } = props.fields + + const buttonsList = buttons?.filter(isEntryTypeButton) || [] + + return ( +
+
+ + + + + +
+ +
+ {text ? : null} + +
+ {caption} + + {isAsset(image) && image.fields.file?.url ? ( +
+ {image.fields.title + {image.fields.description} +
+ ) : null} +
+ +
+ +
+
+
+ ) +} + +export default Card diff --git a/src/components/Foundry/POCs/index.tsx b/src/components/Foundry/POCs/index.tsx new file mode 100644 index 000000000..23d351a1a --- /dev/null +++ b/src/components/Foundry/POCs/index.tsx @@ -0,0 +1,39 @@ +import { Container, Grid, Typography } from '@mui/material' +import RichText from '@/components/common/RichText' +import Card from '@/components/Foundry/POCs/Card' +import { type BaseBlockEntry } from '@/config/types' +import { isEntryTypeBaseBlock } from '@/lib/typeGuards' +import layoutCss from '@/components/common/styles.module.css' +import css from './styles.module.css' + +const POCs = (props: BaseBlockEntry) => { + const { caption, title, text, items } = props.fields + + const itemsList = items?.filter(isEntryTypeBaseBlock) || [] + + return ( + +
+ +
+ + {text && ( +
+ +
+ )} + + + {itemsList.map((item, index) => ( + + + + ))} + + + {caption} +
+ ) +} + +export default POCs diff --git a/src/components/Foundry/POCs/styles.module.css b/src/components/Foundry/POCs/styles.module.css new file mode 100644 index 000000000..75886f69a --- /dev/null +++ b/src/components/Foundry/POCs/styles.module.css @@ -0,0 +1,107 @@ +.title { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.text { + margin: 16px auto 0; + max-width: 725px; + text-align: center; +} + +.title p, +.text p { + margin: 0; +} + +.caption { + margin-top: 80px; + text-align: center; +} + +.gridContainer { + justify-content: center; + margin-top: 40px; +} + +.cardHeader { + height: 100px; + padding: 32px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border: 1px solid var(--mui-palette-border-light); + border-top-left-radius: 16px; + border-top-right-radius: 16px; +} + +.cardHeader p, +.cardBody p { + margin-top: 0; + margin-bottom: auto; +} + +.cardHeader svg { + width: 24px; + height: 24px; +} + +.cardBody { + padding: 32px; + border: 1px solid var(--mui-palette-border-light); + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; +} + +.cardBody a { + text-decoration: underline; + color: var(--mui-palette-primary-main); +} + +.extraText { + margin-top: 24px; +} + +.partner { + height: 32px; + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +} + +.partner img { + width: 32px; + height: 32px; +} + +.buttons { + margin-top: 32px; + display: flex; + justify-content: flex-start; +} + +@media (min-width: 900px) { + .buttons { + justify-content: center; + } + + .gridContainer { + margin-top: 80px; + } + + .card { + display: flex; + flex-direction: column; + height: 100%; + } + + .cardBody { + display: flex; + flex-direction: column; + flex-grow: 1; + } +} diff --git a/src/components/common/Header/navCategories.tsx b/src/components/common/Header/navCategories.tsx index d3a0dc5ce..9525b329b 100644 --- a/src/components/common/Header/navCategories.tsx +++ b/src/components/common/Header/navCategories.tsx @@ -12,6 +12,7 @@ import CareersIcon from '@/public/images/Header/careers-icon.svg' import PressRoomIcon from '@/public/images/Header/press-room-icon.svg' import HelpCenterIcon from '@/public/images/Header/help-center-icon.svg' import GasStationIcon from '@/public/images/Header/gas-station-icon.svg' +import SafeFoundryIcon from '@/public/images/Header/safe-foundry-icon.svg' export type NavItem = { label: string @@ -66,6 +67,11 @@ export const navCategories: NavCategory[] = [ href: AppRoutes.gasStation, icon: , }, + { + label: 'Safe{Foundry}', + href: AppRoutes.foundry, + icon: , + }, ], }, { diff --git a/src/components/commonCMS/CenteredTextBlock/index.tsx b/src/components/commonCMS/CenteredTextBlock/index.tsx new file mode 100644 index 000000000..b50edf492 --- /dev/null +++ b/src/components/commonCMS/CenteredTextBlock/index.tsx @@ -0,0 +1,39 @@ +import { Container, Typography } from '@mui/material' +import { type BaseBlockEntry } from '@/config/types' +import RichText from '@/components/common/RichText' +import layoutCss from '@/components/common/styles.module.css' +import css from './styles.module.css' +import ButtonsWrapper from '@/components/Token/ButtonsWrapper' +import { isEntryTypeButton } from '@/lib/typeGuards' + +const CenteredTextBlock = (props: BaseBlockEntry) => { + const { caption, title, text, buttons } = props.fields + + const buttonsList = buttons?.filter(isEntryTypeButton) || [] + + return ( + + {caption} + +
+
+ +
+ + {text && ( +
+ +
+ )} +
+ + {buttonsList.length > 0 && ( +
+ +
+ )} +
+ ) +} + +export default CenteredTextBlock diff --git a/src/components/commonCMS/CenteredTextBlock/styles.module.css b/src/components/commonCMS/CenteredTextBlock/styles.module.css new file mode 100644 index 000000000..2caf7a8c5 --- /dev/null +++ b/src/components/commonCMS/CenteredTextBlock/styles.module.css @@ -0,0 +1,22 @@ +.title { + margin-top: 16px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.text { + margin: 24px auto 0; + max-width: 725px; +} + +.text p { + margin: 0; +} + +.buttons { + margin-top: 40px; + display: flex; + justify-content: center; +} diff --git a/src/components/commonCMS/Faq/index.tsx b/src/components/commonCMS/Faq/index.tsx new file mode 100644 index 000000000..57cb584a7 --- /dev/null +++ b/src/components/commonCMS/Faq/index.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react' +import { + Accordion, + AccordionDetails, + type AccordionProps, + AccordionSummary, + Container, + Grid, + Typography, +} from '@mui/material' +import PlusIcon from '@/public/images/plus-sign-icon.svg' +import MinusIcon from '@/public/images/minus-sign-icon.svg' +import { isEntryTypeBaseBlock } from '@/lib/typeGuards' +import RichText from '@/components/common/RichText' +import { type BaseBlockEntry } from '@/config/types' +import layoutCss from '@/components/common/styles.module.css' +import css from './styles.module.css' + +const Faq = (props: BaseBlockEntry) => { + const { title, items } = props.fields + + // Tracks which accordion is open + const [openMap, setOpenMap] = useState>() + + const itemsList = items?.filter(isEntryTypeBaseBlock) ?? [] + + return ( + + +
+ + + + + + + {itemsList.map((item, index) => { + const { title, text } = item.fields + + const handleChange: AccordionProps['onChange'] = (_, expanded) => { + setOpenMap((prev) => ({ + ...prev, + [index]: expanded, + })) + } + const expanded = openMap?.[index] ?? false + + return ( + + : } + onClick={() => { + !expanded + }} + > + + + + + {text && } + + ) + })} + + + + ) +} + +export default Faq diff --git a/src/components/commonCMS/Faq/styles.module.css b/src/components/commonCMS/Faq/styles.module.css new file mode 100644 index 000000000..d66527ecf --- /dev/null +++ b/src/components/commonCMS/Faq/styles.module.css @@ -0,0 +1,57 @@ +.gridContainer { + position: relative; +} + +.spot { + position: absolute; + left: -370px; + top: -330px; + z-index: -1; + width: 800px; + height: 800px; + background-image: radial-gradient(rgba(18, 255, 128, 0.5), rgba(18, 19, 18, 1) 80%); + filter: blur(50px); +} + +.accordion { + background-color: unset; + box-shadow: none; + border-bottom: 1px solid var(--mui-palette-border-main); +} + +.accordion :global .MuiAccordionSummary-root { + padding: 0; + display: flex; + gap: 40px; +} + +.accordion :global .MuiAccordionSummary-content { + margin: 32px 0; +} + +.accordion :global .MuiAccordionDetails-root { + padding: 0 0 32px; +} + +.details :global .MuiAccordionDetails-root a { + text-decoration: underline; +} + +/* Resets the paragraph margins on accordion's summary and details */ +.accordion :global .MuiAccordionSummary-content p { + margin: 0; +} + +.details p:first-of-type { + margin-top: 0; +} + +.details p:last-of-type { + margin-bottom: 0; +} + +@media (max-width: 600px) { + .spot { + width: 700px; + } +} diff --git a/src/config/routes.ts b/src/config/routes.ts index 629958c2f..691e36799 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -11,6 +11,7 @@ export const AppRoutes = { imprint: '/imprint', governance: '/governance', gasStation: '/gas-station', + foundry: '/foundry', ecosystem: '/ecosystem', disclaimer: '/disclaimer', core: '/core',