diff --git a/i18n/en-US/code.json b/i18n/en-US/code.json index 426ad16..db9454a 100644 --- a/i18n/en-US/code.json +++ b/i18n/en-US/code.json @@ -215,6 +215,15 @@ "team.monica.position": { "message": "Back-end Developer" }, + "partners.eyebrow": { + "message": "Backing JSConf Brasil 2026" + }, + "partners.cta.text": { + "message": "There's room for your brand here too." + }, + "partners.cta.button": { + "message": "Become a sponsor" + }, "sponsors.pageTitle": { "message": "Sponsors" }, diff --git a/i18n/es-419/code.json b/i18n/es-419/code.json index e33539d..9a04682 100644 --- a/i18n/es-419/code.json +++ b/i18n/es-419/code.json @@ -215,6 +215,15 @@ "team.monica.position": { "message": "Desarrolladora Back-end" }, + "partners.eyebrow": { + "message": "Apoyan JSConf Brasil 2026" + }, + "partners.cta.text": { + "message": "Tu marca también cabe aquí." + }, + "partners.cta.button": { + "message": "Quiero patrocinar" + }, "sponsors.pageTitle": { "message": "Patrocinadores" }, diff --git a/i18n/pt-BR/code.json b/i18n/pt-BR/code.json index 9260b8a..3e24562 100644 --- a/i18n/pt-BR/code.json +++ b/i18n/pt-BR/code.json @@ -215,6 +215,15 @@ "team.lucas.position": { "message": "Principal Staff Engineer" }, + "partners.eyebrow": { + "message": "Apoiam a JSConf Brasil 2026" + }, + "partners.cta.text": { + "message": "Sua marca também cabe aqui." + }, + "partners.cta.button": { + "message": "Quero patrocinar" + }, "sponsors.pageTitle": { "message": "Patrocinadores" }, diff --git a/src/website/assets/img/partners/cod3rs.webp b/src/website/assets/img/partners/cod3rs.webp new file mode 100644 index 0000000..2b7fa49 Binary files /dev/null and b/src/website/assets/img/partners/cod3rs.webp differ diff --git a/src/website/assets/img/partners/salvy.webp b/src/website/assets/img/partners/salvy.webp new file mode 100644 index 0000000..8922516 Binary files /dev/null and b/src/website/assets/img/partners/salvy.webp differ diff --git a/src/website/assets/img/partners/unipds.webp b/src/website/assets/img/partners/unipds.webp new file mode 100644 index 0000000..b063ca0 Binary files /dev/null and b/src/website/assets/img/partners/unipds.webp differ diff --git a/src/website/components/home/Partners.tsx b/src/website/components/home/Partners.tsx new file mode 100644 index 0000000..b95191f --- /dev/null +++ b/src/website/components/home/Partners.tsx @@ -0,0 +1,73 @@ +import type { CSSProperties } from 'react'; +import { useRef, useState } from 'react'; +import { ArrowRight } from 'lucide-react'; +import { Text } from '@site/src/website/components/shared/i18n'; +import { SafeLink } from '@site/src/website/components/shared/SafeLink'; +import { link, partners } from '../../configs/definitions'; +import { useScroll } from '../../hooks/useScroll'; +import { Image } from '../shared/Image'; + +export const Partners = () => { + const ref = useRef(null); + const [show, setShow] = useState(false); + const [active, setActive] = useState(0); + + useScroll(ref, (isVisible) => { + if (isVisible) setShow(true); + }); + + return ( +
+
+

+ +

+ +
+
+ {partners.map((partner, index) => ( + setActive(index)} + onFocus={() => setActive(index)} + > + {partner.name} + + ))} +
+ +
+ +
+
+ +
+

+ +

+ + + +
+
+
+ ); +}; diff --git a/src/website/configs/definitions.ts b/src/website/configs/definitions.ts index c2c6d28..e76e596 100644 --- a/src/website/configs/definitions.ts +++ b/src/website/configs/definitions.ts @@ -2,3 +2,34 @@ export const link = { tickets: 'https://guild.host/events/jsconf-brasil-primeira-vdc8dh', sponsors: 'https://forms.gle/SPyyD3SsuurVpvCNA', } as const; + +export type Partner = { + name: string; + logo: string; + url: string; + /** Optical size tuning: logos differ in visual mass at the same box size. */ + scale?: number; +}; + +export const partners: Partner[] = [ + { + // Compact logotype (1.9:1) — fills the height cap long before the others. + name: 'Salvy', + logo: '/img/partners/salvy.webp', + url: 'https://salvy.com.br/', + scale: 0.95, + }, + { + // Very wide wordmark (6.3:1) — width-bound rather than height-bound. + name: 'Cod3rs', + logo: '/img/partners/cod3rs.webp', + url: 'https://www.coders.com.br/', + scale: 0.8, + }, + { + name: 'UniPDS Educação', + logo: '/img/partners/unipds.webp', + url: 'https://unipds.com.br/', + scale: 0.7, + }, +]; diff --git a/src/website/pages/index.tsx b/src/website/pages/index.tsx index ce122e6..583ac04 100644 --- a/src/website/pages/index.tsx +++ b/src/website/pages/index.tsx @@ -2,8 +2,8 @@ import { Benefits } from '../components/home/Benefits'; import { Gallery } from '../components/home/Gallery'; import { Home } from '../components/home/Home'; import { Location } from '../components/home/Location'; +import { Partners } from '../components/home/Partners'; // import { Speakers } from '../components/home/Speakers'; -import { Sponsors } from '../components/home/Sponsors'; import { TicketSelection } from '../components/home/TicketSelection'; import { Waitlist } from '../components/home/Waitlist'; import { Page } from '../components/shared/Page'; @@ -11,12 +11,12 @@ import { Page } from '../components/shared/Page'; export default () => ( + {/* */} - ); diff --git a/src/website/scss/pages/_partners.scss b/src/website/scss/pages/_partners.scss new file mode 100644 index 0000000..129093a --- /dev/null +++ b/src/website/scss/pages/_partners.scss @@ -0,0 +1,288 @@ +@use '../global/mixins' as *; + +// A beam crossing the bay when it lights up — the rail reading that channel. +@keyframes partner-sweep { + 0% { + transform: translateX(-140%) skewX(-12deg); + opacity: 0; + } + 25% { + opacity: 1; + } + 100% { + transform: translateX(280%) skewX(-12deg); + opacity: 0; + } +} + +// Overshoots past the resting hover position, then settles back into it. +@keyframes partner-power-on { + 0% { + transform: scale(var(--logo-scale, 1)); + } + 55% { + transform: translateY(-0.9rem) scale(calc(var(--logo-scale, 1) * 1.1)); + } + 100% { + transform: translateY(-0.5rem) scale(calc(var(--logo-scale, 1) * 1.06)); + } +} + +@mixin partners { + &#partners { + // Sits directly under the hero, so it stays low-profile on purpose. + // The hero ends on its own credit block ("Realização"), so this needs real + // air above it or the two credit lines read as one stack. + padding-top: 7rem; + padding-bottom: 9rem; + + $ink: #f5f6fa; + $mute: #6f7b72; + $signal: var(--ifm-color-primary); + + & > .content { + gap: 2.4rem; + max-width: 112rem; + } + + .eyebrow { + margin: 0; + font-family: var(--ifm-font-family-title); + font-size: 1.2rem; + font-weight: 700; + // Widest tracking on the site: reads as a rail marker, not a heading. + letter-spacing: 0.32em; + text-transform: uppercase; + color: $mute; + } + + .rail { + position: relative; + width: 100%; + } + + .bays { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + background-color: rgba(141, 248, 34, 0.03); + } + + .bay { + @include flex(row, center, center); + position: relative; + height: 13rem; + padding-inline: 2rem; + overflow: hidden; + text-decoration: none; + transition: background-color 0.35s var(--ease-out-quad); + + // Green wash behind the lit partner, faded at the edges so the bay glows + // rather than turning into a coloured box. + &::before { + content: ''; + position: absolute; + inset: 0; + background-image: radial-gradient( + ellipse at center, + rgba(141, 248, 34, 0.1), + transparent 72% + ); + opacity: 0; + transition: opacity 0.35s var(--ease-out-quad); + pointer-events: none; + } + + // Driven by :hover/:focus only — never by .active. A sticky class would + // leave the bay lit after the pointer left, and would stop the animations + // below from ever replaying on a second visit. + &:hover::before, + &:focus-visible::before { + opacity: 1; + } + + // The sweeping beam itself, parked off-frame until the bay lights up. + &::after { + content: ''; + position: absolute; + inset-block: 0; + left: 0; + width: 38%; + background-image: linear-gradient( + 100deg, + transparent, + rgba(141, 248, 34, 0.2), + rgba(245, 246, 250, 0.14), + transparent + ); + opacity: 0; + transform: translateX(-140%) skewX(-12deg); + pointer-events: none; + } + + &:hover::after, + &:focus-visible::after { + animation: partner-sweep 0.75s var(--ease-out-quad); + } + + & + .bay { + border-left: 0.1rem solid rgba(141, 248, 34, 0.08); + } + + &:hover, + &:focus-visible { + text-decoration: none; + } + + &:focus-visible { + outline: 0.2rem solid $signal; + outline-offset: -0.2rem; + } + } + + .logo { + position: relative; + max-width: 100%; + max-height: 7rem; + object-fit: contain; + opacity: 0.55; + // Logos differ in visual mass at the same box size; --logo-scale evens them out. + transform: scale(var(--logo-scale, 1)); + // At rest every logo is one neutral ink; colour is the reward for attention. + filter: grayscale(1) brightness(1.05); + transition: + opacity 0.35s var(--ease-out-quad), + filter 0.35s var(--ease-out-quad), + transform 0.35s var(--ease-back-out-soft); + } + + .bay:hover .logo, + .bay:focus-visible .logo { + opacity: 1; + // Drops the greyscale, so the partner's real brand colours come back. + filter: drop-shadow(0 0 1.8rem rgba(141, 248, 34, 0.3)); + transform: translateY(-0.5rem) scale(calc(var(--logo-scale, 1) * 1.06)); + animation: partner-power-on 0.55s var(--ease-out-quad); + } + + .track { + position: relative; + height: 0.2rem; + background-color: rgba(141, 248, 34, 0.18); + transform: scaleX(0); + transform-origin: left center; + transition: transform 0.8s var(--ease-out-quad); + } + + .content.show .track { + transform: scaleX(1); + } + + .segment { + position: absolute; + inset-block: 0; + left: 0; + width: calc(100% / var(--bays, 1)); + background-color: $signal; + box-shadow: 0 0 1.2rem rgba(141, 248, 34, 0.5); + transform: translateX(calc(var(--active, 0) * 100%)); + transition: transform 0.45s var(--ease-back-out-soft); + } + + // A quiet footnote under the rail, not a fourth partner competing with them. + .invite { + @include flex(row, center, center, wrap); + gap: 0.2rem 0.8rem; + margin-top: 0.4rem; + } + + .invite-text { + margin: 0; + font-size: 1.3rem; + color: $mute; + } + + .invite-button { + @include flex(row, center, center); + gap: 0.5rem; + font-size: 1.3rem; + font-weight: 600; + color: $signal; + text-decoration: none; + text-underline-offset: 0.4rem; + + &:hover, + &:focus-visible { + color: $signal; + text-decoration: underline; + } + + &:focus-visible { + outline: 0.2rem solid $signal; + outline-offset: 0.4rem; + border-radius: 0.2rem; + } + + .icon { + width: 1.3rem; + height: 1.3rem; + } + } + + @media (max-width: 630px) { + padding-bottom: 6rem; + + .bays { + grid-auto-flow: row; + } + + .bay { + height: 10rem; + border-bottom: 0.2rem solid rgba(141, 248, 34, 0.12); + + & + .bay { + border-left: 0; + } + + &.active { + border-bottom-color: var(--ifm-color-primary); + } + } + + .logo { + max-height: 5.6rem; + } + + // The sliding rail needs a horizontal row; stacked bays carry it themselves. + .track { + display: none; + } + } + + @media (prefers-reduced-motion: reduce) { + .track, + .segment, + .logo, + .bay, + .bay::before { + transition: none; + } + + .track { + transform: scaleX(1); + } + + // Colour and glow still resolve; only the movement is dropped. + .bay:hover .logo, + .bay:focus-visible .logo { + transform: scale(var(--logo-scale, 1)); + animation: none; + } + + .bay:hover::after, + .bay:focus-visible::after { + animation: none; + } + } + } +} diff --git a/src/website/scss/pages/root.scss b/src/website/scss/pages/root.scss index fdc5d88..52baa51 100644 --- a/src/website/scss/pages/root.scss +++ b/src/website/scss/pages/root.scss @@ -4,6 +4,7 @@ @use 'speakers' as *; @use 'benefits' as *; @use 'gallery' as *; +@use 'partners' as *; @use 'location' as *; @use 'waitlist' as *; @use 'ticket-selection' as *; @@ -49,6 +50,7 @@ @include home; @include benefits; @include gallery; + @include partners; @include speakers; @include location; @include waitlist;