From aa0a02659d19975cede64174677b1983a91a7282 Mon Sep 17 00:00:00 2001 From: xSatori <99294685+xSatori@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:07:26 -0400 Subject: [PATCH 01/13] Refine about page and build support --- apps/web/package.json | 1 + apps/web/public/builderlogo.png | Bin 0 -> 4241 bytes apps/web/public/images/about-divider.svg | 48 + apps/web/src/modules/about/AboutPage.css.ts | 1318 +++++++++++++++++ apps/web/src/modules/about/AboutPage.tsx | 152 ++ .../about/components/AboutFinalCta.tsx | 56 + .../modules/about/components/AboutHero.tsx | 120 ++ .../modules/about/components/CoiningCard.tsx | 59 + .../components/CoiningHighlightsSection.tsx | 34 + .../src/modules/about/components/DaoCard.tsx | 73 + .../components/DroposalHighlightsSection.tsx | 101 ++ .../components/EcosystemActivitySection.tsx | 40 + .../about/components/EcosystemStatGrid.tsx | 33 + .../about/components/FeaturedDaoSection.tsx | 82 + .../about/components/HowItWorksSection.tsx | 35 + .../components/ProposalHighlightsSection.tsx | 25 + .../modules/about/components/SectionIntro.tsx | 26 + .../about/components/WhatIsBuilderSection.tsx | 43 + .../components/WhatYouCanBuildSection.tsx | 44 + .../about/components/WhyBuilderSection.tsx | 39 + apps/web/src/modules/about/data.ts | 288 ++++ apps/web/src/modules/about/index.ts | 1 + apps/web/src/modules/about/types.ts | 111 ++ apps/web/src/modules/about/useAboutDaoTabs.ts | 47 + .../web/src/modules/about/useAboutShowcase.ts | 44 + .../modules/about/useAboutSnapshotStats.ts | 44 + apps/web/src/modules/about/utils.ts | 16 + apps/web/src/modules/dashboard/Dashboard.tsx | 4 +- .../modules/dashboard/SingleDaoSelector.tsx | 12 +- apps/web/src/pages/about.tsx | 72 +- apps/web/src/pages/api/about/dao-tabs.ts | 563 +++++++ apps/web/src/pages/api/about/showcase.ts | 396 +++++ apps/web/src/pages/api/about/snapshot.ts | 264 ++++ .../web/src/pages/api/ai/generateTxSummary.ts | 38 +- apps/web/src/pages/api/migrated.ts | 3 +- .../src/pages/dao/[network]/[token]/index.tsx | 11 +- .../dao/[network]/[token]/proposal/create.tsx | 34 +- apps/web/src/styles/about.css.ts | 17 - apps/web/src/utils/api/ai/summaries.ts | 84 ++ pnpm-lock.yaml | 15 +- turbo.json | 2 + 41 files changed, 4241 insertions(+), 154 deletions(-) create mode 100644 apps/web/public/builderlogo.png create mode 100644 apps/web/public/images/about-divider.svg create mode 100644 apps/web/src/modules/about/AboutPage.css.ts create mode 100644 apps/web/src/modules/about/AboutPage.tsx create mode 100644 apps/web/src/modules/about/components/AboutFinalCta.tsx create mode 100644 apps/web/src/modules/about/components/AboutHero.tsx create mode 100644 apps/web/src/modules/about/components/CoiningCard.tsx create mode 100644 apps/web/src/modules/about/components/CoiningHighlightsSection.tsx create mode 100644 apps/web/src/modules/about/components/DaoCard.tsx create mode 100644 apps/web/src/modules/about/components/DroposalHighlightsSection.tsx create mode 100644 apps/web/src/modules/about/components/EcosystemActivitySection.tsx create mode 100644 apps/web/src/modules/about/components/EcosystemStatGrid.tsx create mode 100644 apps/web/src/modules/about/components/FeaturedDaoSection.tsx create mode 100644 apps/web/src/modules/about/components/HowItWorksSection.tsx create mode 100644 apps/web/src/modules/about/components/ProposalHighlightsSection.tsx create mode 100644 apps/web/src/modules/about/components/SectionIntro.tsx create mode 100644 apps/web/src/modules/about/components/WhatIsBuilderSection.tsx create mode 100644 apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx create mode 100644 apps/web/src/modules/about/components/WhyBuilderSection.tsx create mode 100644 apps/web/src/modules/about/data.ts create mode 100644 apps/web/src/modules/about/index.ts create mode 100644 apps/web/src/modules/about/types.ts create mode 100644 apps/web/src/modules/about/useAboutDaoTabs.ts create mode 100644 apps/web/src/modules/about/useAboutShowcase.ts create mode 100644 apps/web/src/modules/about/useAboutSnapshotStats.ts create mode 100644 apps/web/src/modules/about/utils.ts create mode 100644 apps/web/src/pages/api/about/dao-tabs.ts create mode 100644 apps/web/src/pages/api/about/showcase.ts create mode 100644 apps/web/src/pages/api/about/snapshot.ts delete mode 100644 apps/web/src/styles/about.css.ts create mode 100644 apps/web/src/utils/api/ai/summaries.ts diff --git a/apps/web/package.json b/apps/web/package.json index 1dea5e859..dc0869166 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "flatpickr": "^4.6.13", "formik": "^2.4.6", "framer-motion": "^12.12.1", + "graphql-request": "^7.1.2", "ioredis": "^5.8.1", "next": "^15.5.9", "nextjs-progressbar": "^0.0.16", diff --git a/apps/web/public/builderlogo.png b/apps/web/public/builderlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..1b06d6e5cadee1306168527b712f0e807bd9d707 GIT binary patch literal 4241 zcmchaYh2RVy2t-&lTBl~D`&Jabr)WCI@xF`hUnyEikVu8mrQWdG*i4QiJ~<&2pW95jqloW4x$!$j7O7jYMzkvb)q7sV7&VI4qofrGeIWN}b#riy-XFbpQ{MPUL zz|-A%-`>M}0RY$sb2)zn08}$~_HXv=-kGD4+g|M~Uq-w5#R0%q2R{!LfK;dp0Dt=d zcK)2#ok9Vp7YABA*Cj^66FmA?dhVbF0itnOG$Oiim($;mS!8NEJvz{?@@2aNpfi>8 z5FG93^57vj=KeuWoMYzw@6Yd_F#;L>WEgV*Vw^*+Wi}y1!j%;_k5=srzIeQkuyM15 zQ4-={P&!yVT8e2@@G7Gm;A9$#4FGLNhRXDJvg;yv?gE~;n5qJvAZ-=E^uSL3+Fv>W zK<52p0N~`fAGmw0Xb zf>Wa;BT?wrBpc48hj;1%dR`5*0mBFVs#V0 zTV4T5BhICFkJ_(St>KW+`Mu`%zV1}&8jrcg+OuwT22Ib^Kc0)g%gooMfkD+40Oq{u zTGwe70>5mguWr&^3xdEjYdFhyUuR)e-W_+Wb{LP>sdYbKLbkP3=e687SKJWGZyL|Bns3^@6Vw-Se`lLFPw(3cFH)F+`VMvP8-ZF*CK&x+*E-8izl!$@MXrr|%NR zIX-gFgFkHzWxzgv`cRE=`|~T7-=Uqac0`Tkt?&Fjx1QLdTHUFn0K1P$aXd4l$GhZqU^chbKCZ8o8cz#9SKWEDxju7=w?3niVC4A z$7FCzc&~8VQ}V{bq_{Vjq+}X)*KPbnw|6-iVJu2Av_+p7LtNBc*!r^bIq$~$>+G15 z$d*C_q%x-RdIMhETfmAQrTRuJvLf&ig)nOa_rB(mCWCT|x7Z=IT@xpFu0G6VZo5g2 z;7oKFc&`30GoOxWZwsCK7ZeVgx7Vo2C&xG|NYlR8Ts3){4k6A1)Zt_*8C+m3oJv}W zm*k_3JnVkAtEae)EAQV3Kv!?fWF^b{`sn!x1%s2qPd&t2=AA^nk6z$!5ic;5_xZU$BrfKZZ-+Ps>zR}6f2p0E%nBqWOG zsHq;7N714W9m_5aK{2SATh)JTJd(0Z3}Ahg78DxA4mmWFVblF4wfu z`9&PEzeR%CX{fEV{s0PnX=d%3WXanmO&yAD+07={S#ekk%CEvIw~?{Q&HTSJt?21< zE6Id(Zm_+-?AG0;cZ0aA#G7Y5*4*3v(H8@8fsFdfP6;OhfA>n=^D$%rWr-q)UrtKV zQP)E441&FJ1Dfs?7_f*F81x;wKpC*6(wyTtm#~1+$->%r)Pl9Rak-2o|Difo`Jxm( zoEXQbAf@(CHu4rKUG7iBX{-M46!1or*Jmw_rtZ(g79I4XohWs)1P~skYn}0F;*50L zi>Ig{!j{eUI#6G=r?W*@1PV(y<{VLt2Ol+=Ehy910J?etht#IN9dCR=m8al&O1m&0 z=wbIv`P1IuG*a!=*+S*dOe0~K-a(xw=<=Poow$jflvv0tIc%WL`HVqOC0F zqN#+gr~fjPH}eA9k)|c`%7q~9@aADg`|s)>%*4jjtL}1UHLFf7ek=?9i~g;Di|Xz& z{Wj?3ZzHiIEz|lruWBX@;ElErfc)2pQ%Sq7Ayb6_r!*3 zQtw-!fb|E2Bbgaxx^&*@S#hc{qbonhBrH_wm5`MqfhnCt))I!5PC}J$FXTIrPiqN->f}uZuKPYPQRDybXN%;D|!qfu$#sj zRvjz1maZfs>WO2@RDNx3ZjM0k*rNmn4^_MxE5xoi>fh;aqQ|VYe{g)%6n&>Lsj-#H zHOrH}wd!A7c$Q~1(U6O&C-Q&27GrJe0wK?HG7S2g9K`Q`bB`*?hR58cfE$W@OCa0? z#r$%^=G=O$Onv(i*UTeyCr-!*s%POP<5K?Fro735^a6v6XRh}zt>$YikH6Ir`I1j+ zcR?5u$<_Y3XWLsBA_G!S%#xSGh8w?LER)780(c8I8d3(+vN67jKT7wPicy{+L^!)#&FAqtyn}-B!n$WJ*EY<7s+JA@`-81 zY+$IvNZRI=#HtjN1S!=oZ3=T`$eI3!uaT5zM^CqA{rqwa@>8~Iv=({A(+!`_-CAkc zbTwvB!?$G?6eRDl&`~z#z_2Lrh+xiVZ@ zHo4Vx@lcMYrT^<;6<;Eu^(6dE0%_V1ZgoxODRhP71~f8CNrY`ujL0h?7xV6EdK-JjWE6cP6=EI0T?3nc{X2K{r;JjmIk`-!`CPFH!oS@CVs z?M4RwM-G!$51H~V(|;rO_F+A7y^y~14P>|56=O&t_U>nxO#XCp+#s`)Lw?}2)G#Z4 zgqa_h2VKG!EA}$I;rVqaMABNr3Ic1Z7+JT1fNOfGU^u$zJ9o;C`GMs2VCo;kZD8Yb z&&Co%N+t_d5U!+2f_HZ z&-;xE3bN8sxSkzbV!M9cQrBp3Rn%Fp^Uc096k`j4@g#Eq+ny8+&40P&*F#$tDtfYf zF38(AQYukc@St;9u;8|(ZsNt~*mwUjB`XWUg&$z_VeESzD_o^{LcFI_XM52MR)^=D ztwC9bckCz`XOmUS!H9o$>lzs-_sf&}kw6KI!%~KakroQX!02Wk$?{Bz6;-cbr$z|s zl^}ort*<(Hsq5eOrI{Xz1QFm`OS)wQE=;nL9Xw$?*-DJ4GE#>&p05nUKX;GhjJ}y? zcE$4APt0~1c%0<8{nAkhQamA>&|x((#$zeqq^SU|&QWUfJO8fZ+Um>Ozek!69%qMG zm0KCS3>!8sRacaTCJX4>1B8y(d(HbanJ)+&dzWES@L3VkRG)Hl!nXb~tI3{Fc*G=q z_PgFmVq*0!dL3C~xWZIvo`AO=qb8P3+q~v-jt*xzl~e3O;X_7Pz0N%2&spFtF2`7Sad`{u*|{~urzXtM^Cn7_ZEi#%zpr{#b|z|&shM$abaCB~ zvzXmpm~`f!A&>25NejwLDeZ)qLX4^}9_M8QY7?PhBye|ZzS$$k4{piG_+b^O5e zuD@kABdDo?@{muBHUEUf;TI~~RC69s0yb5FHs-Ov7!4K0t^=S$#)m5Im>24W!*=^A zL(efsbpYrwZAH+-;Nz_PuW{*w`F0SScPuiWs$SgL0xaz>8Z{t*82IZjON7`LY>RM{ zxZ<}!QX^q#)MxB^dYhd%6a?H2s@fq5+dv^m?#Qm2JWCTdd&A*rF9&4PhSj?7hi@yEX~_wIc7z?Nx@pEuUdfrUfmlHgi_I~WCgkLw{c zbFaQjXk6~e<3+pCjYNyt#6!PkghbW1B0u;EYiFY#gnuqnBQ-Bh1=z6tzm(DU+iG!_ XM>mTe>m1&pcmU?)e!kK%@TdO*pqKjI literal 0 HcmV?d00001 diff --git a/apps/web/public/images/about-divider.svg b/apps/web/public/images/about-divider.svg new file mode 100644 index 000000000..00941d7ae --- /dev/null +++ b/apps/web/public/images/about-divider.svg @@ -0,0 +1,48 @@ + + image + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts new file mode 100644 index 000000000..ea7481e9b --- /dev/null +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -0,0 +1,1318 @@ +import { keyframes, style } from '@vanilla-extract/css' + +const focusRing = { + outline: '3px solid rgba(37, 99, 235, 0.28)', + outlineOffset: '3px', +} + +const marqueeScroll = keyframes({ + '0%': { + transform: 'translateX(0)', + }, + '100%': { + transform: 'translateX(-50%)', + }, +}) + +export const page = style({ + width: '100%', + padding: '48px 16px 96px', + boxSizing: 'border-box', + background: '#FFFFFF', + '@media': { + 'screen and (min-width: 768px)': { + padding: '64px 24px 120px', + }, + }, +}) + +export const container = style({ + maxWidth: '1180px', + margin: '0 auto', +}) + +export const centeredImageWrap = style({ + display: 'flex', + justifyContent: 'center', +}) + +export const centeredImage = style({ + width: '280px', + maxWidth: '100%', + height: 'auto', + display: 'block', +}) + +export const heroVennWrap = style({ + width: '100%', + height: '100%', + minHeight: '220px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '12px', +}) + +export const heroVennImage = style({ + width: '100%', + maxWidth: '260px', + height: 'auto', + display: 'block', +}) + +export const section = style({ + marginTop: '72px', + '@media': { + 'screen and (min-width: 768px)': { + marginTop: '96px', + }, + }, +}) + +export const sectionHeader = style({ + display: 'flex', + flexDirection: 'column', + gap: '12px', + marginBottom: '28px', + maxWidth: '720px', +}) + +export const eyebrow = style({ + display: 'inline-flex', + width: 'fit-content', + borderRadius: '999px', + padding: '6px 12px', + background: '#FFFFFF', + border: '1px solid #E6E3D7', + fontSize: '12px', + lineHeight: '16px', + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: '#2563EB', + fontWeight: 700, +}) + +export const sectionTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '32px', + lineHeight: 1.05, + whiteSpace: 'nowrap', + color: '#111111', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '44px', + }, + }, +}) + +export const sectionTitleOnly = style({ + marginBottom: '12px', +}) + +export const sectionCopy = style({ + fontSize: '17px', + lineHeight: 1.6, + color: '#5C5648', +}) + +export const introCopyNoWrap = style({ + fontSize: '17px', + lineHeight: 1.6, + color: '#5C5648', + whiteSpace: 'nowrap', +}) + +export const hero = style({ + display: 'grid', + gap: '28px', + alignItems: 'stretch', + '@media': { + 'screen and (min-width: 980px)': { + gridTemplateColumns: 'minmax(0, 1.1fr) minmax(360px, 0.9fr)', + gap: '40px', + }, + }, +}) + +export const heroCopy = style({ + display: 'flex', + flexDirection: 'column', + gap: '20px', + paddingTop: '8px', +}) + +export const heroTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '44px', + lineHeight: 0.98, + letterSpacing: '-0.03em', + color: '#111111', + maxWidth: '760px', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '60px', + }, + }, +}) + +export const heroText = style({ + maxWidth: '640px', + fontSize: '18px', + lineHeight: 1.65, + color: '#4C463B', +}) + +export const heroHighlightList = style({ + display: 'grid', + gap: '10px', + margin: 0, + padding: 0, + listStyle: 'none', + marginTop: '-4px', +}) + +export const heroHighlight = style({ + display: 'flex', + alignItems: 'center', + gap: '10px', + fontSize: '15px', + lineHeight: 1.5, + color: '#232018', +}) + +export const heroHighlightDot = style({ + width: '10px', + height: '10px', + borderRadius: '999px', + background: '#2563EB', + flexShrink: 0, +}) + +export const primaryButton = style({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '42px', + padding: '10px 16px', + borderRadius: '999px', + border: '1px solid #2563EB', + color: '#FFFFFF', + background: '#2563EB', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + borderColor: '#2563EB', + boxShadow: 'inset 0 0 0 1px #2563EB', + backgroundColor: '#EFF6FF', + color: '#1E3A8A', + }, + '&:focus-visible': focusRing, + }, +}) + +export const heroActions = style({ + display: 'flex', + flexDirection: 'column', + gap: '12px', + alignItems: 'stretch', + '@media': { + 'screen and (min-width: 640px)': { + flexDirection: 'row', + alignItems: 'center', + }, + }, +}) + +export const secondaryButton = style({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '42px', + padding: '10px 16px', + borderRadius: '999px', + border: '1px solid #DBEAFE', + color: '#1E3A8A', + background: '#FFFFFF', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + cursor: 'pointer', + backgroundColor: '#EFF6FF', + borderColor: '#93C5FD', + }, + '&:focus-visible': focusRing, + }, +}) + +export const heroPanel = style({ + position: 'relative', + overflow: 'hidden', + borderRadius: '28px', + border: '1px solid #E6E1D4', + background: '#F8FBFF', + padding: '22px', + minHeight: '420px', + boxShadow: '0 10px 24px rgba(17, 17, 17, 0.04)', +}) + +export const heroPanelGlow = style({ + position: 'absolute', + inset: 0, + background: + 'radial-gradient(circle at 20% 20%, rgba(147, 197, 253, 0.18), transparent 24%), radial-gradient(circle at 80% 10%, rgba(191, 219, 254, 0.18), transparent 18%)', + pointerEvents: 'none', +}) + +export const montageGrid = style({ + position: 'relative', + zIndex: 1, + display: 'grid', + gap: '14px', + gridTemplateColumns: '1.05fr 0.95fr', + gridTemplateAreas: '"primary side" "footer footer"', + '@media': { + 'screen and (max-width: 540px)': { + gridTemplateColumns: '1fr', + gridTemplateAreas: '"primary" "side" "footer"', + }, + }, +}) + +export const montageCard = style({ + borderRadius: '20px', + border: '1px solid rgba(17, 17, 17, 0.08)', + background: 'rgba(255, 255, 255, 0.92)', + padding: '18px', + boxShadow: '0 6px 16px rgba(17, 17, 17, 0.04)', +}) + +export const montagePrimary = style({ + gridArea: 'primary', + minHeight: '150px', +}) + +export const montageSide = style({ + gridArea: 'side', + minHeight: '220px', +}) + +export const montageSecondary = style({ + gridArea: 'secondary', + minHeight: '150px', +}) + +export const montageFooter = style({ + gridArea: 'footer', + minHeight: '180px', + padding: '10px', +}) + +export const logoMarquee = style({ + width: '100%', + height: '100%', + minHeight: '160px', + overflow: 'hidden', + borderRadius: '16px', + background: + 'linear-gradient(135deg, rgba(239, 246, 255, 0.9) 0%, rgba(255, 255, 255, 0.9) 100%)', +}) + +export const logoMarqueeTrack = style({ + position: 'relative', + overflow: 'hidden', + width: '100%', + height: '100%', + selectors: { + '&::before': { + content: '', + position: 'absolute', + inset: 0, + background: + 'linear-gradient(90deg, rgba(248, 251, 255, 1) 0%, rgba(248, 251, 255, 0) 10%, rgba(248, 251, 255, 0) 90%, rgba(248, 251, 255, 1) 100%)', + zIndex: 2, + pointerEvents: 'none', + }, + }, +}) + +export const logoMarqueeInner = style({ + display: 'flex', + alignItems: 'center', + gap: '18px', + width: 'max-content', + minWidth: '100%', + height: '100%', + padding: '22px 0', + animation: `${marqueeScroll} 28s linear infinite`, +}) + +export const logoMarqueeItem = style({ + width: '86px', + height: '86px', + borderRadius: '22px', + overflow: 'hidden', + background: '#FFFFFF', + border: '1px solid rgba(37, 99, 235, 0.08)', + boxShadow: '0 10px 22px rgba(17, 17, 17, 0.06)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, +}) + +export const logoMarqueeImage = style({ + width: '100%', + height: '100%', + objectFit: 'cover', + display: 'block', +}) + +export const montageLabel = style({ + fontSize: '12px', + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: '#726A5B', + fontWeight: 700, +}) + +export const montageValue = style({ + marginTop: '10px', + fontFamily: 'ptRoot, sans-serif', + fontSize: '28px', + lineHeight: 1, + whiteSpace: 'pre-line', + color: '#111111', +}) + +export const montageBody = style({ + marginTop: '10px', + fontSize: '14px', + lineHeight: 1.55, + color: '#5C5648', +}) + +export const daoMiniList = style({ + display: 'grid', + gap: '10px', + marginTop: '14px', +}) + +export const daoMiniCard = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '12px', + padding: '12px 14px', + borderRadius: '16px', + background: '#EFF6FF', +}) + +export const daoMiniAvatar = style({ + width: '40px', + height: '40px', + borderRadius: '14px', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + fontWeight: 700, + fontSize: '14px', + color: '#111111', +}) + +export const heroFooterStat = style({ + display: 'flex', + flexDirection: 'column', + gap: '6px', +}) + +export const heroFooterValue = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '24px', + color: '#111111', +}) + +export const statGrid = style({ + display: 'grid', + gap: '16px', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + '@media': { + 'screen and (min-width: 640px)': { + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + }, + 'screen and (min-width: 1080px)': { + gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', + }, + }, +}) + +export const statCard = style({ + position: 'relative', + overflow: 'hidden', + minHeight: '178px', + borderRadius: '20px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '22px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + transition: 'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease', + selectors: { + '&:hover': { + transform: 'translateY(-1px)', + borderColor: '#D7D0C0', + boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + }, + }, +}) + +export const statAccent = style({ + position: 'absolute', + top: '18px', + right: '18px', + width: '54px', + height: '54px', + borderRadius: '18px', + opacity: 0.9, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '28px', + lineHeight: 1, +}) + +export const statLabel = style({ + position: 'relative', + zIndex: 1, + fontSize: '13px', + fontWeight: 700, + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: '#6F685A', +}) + +export const statValue = style({ + position: 'relative', + zIndex: 1, + marginTop: '18px', + fontFamily: 'ptRoot, sans-serif', + fontSize: '42px', + lineHeight: 1, + color: '#111111', +}) + +export const statDetail = style({ + position: 'relative', + zIndex: 1, + marginTop: '18px', + maxWidth: '28ch', + fontSize: '14px', + lineHeight: 1.55, + color: '#5A5347', +}) + +export const sectionTopRow = style({ + display: 'flex', + flexDirection: 'column', + gap: '18px', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginBottom: '28px', + '@media': { + 'screen and (min-width: 768px)': { + flexDirection: 'row', + alignItems: 'flex-end', + }, + }, +}) + +export const sectionInlineRow = style({ + display: 'flex', + flexDirection: 'column', + gap: '18px', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginBottom: '28px', + '@media': { + 'screen and (min-width: 960px)': { + flexDirection: 'row', + alignItems: 'center', + }, + }, +}) + +export const sectionInlineCopy = style({ + maxWidth: '760px', + fontSize: '17px', + lineHeight: 1.6, + color: '#5C5648', + margin: 0, +}) + +export const tabs = style({ + display: 'flex', + flexWrap: 'wrap', + gap: '10px', +}) + +export const tabButton = style({ + minHeight: '42px', + borderRadius: '999px', + border: '1px solid #DBEAFE', + background: '#FFFFFF', + padding: '10px 16px', + fontSize: '14px', + fontWeight: 700, + color: '#1E3A8A', + transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + cursor: 'pointer', + borderColor: '#93C5FD', + backgroundColor: '#EFF6FF', + }, + '&:focus-visible': focusRing, + }, +}) + +export const activeTabButton = style({ + background: '#2563EB', + borderColor: '#2563EB', + color: '#FFFFFF', + selectors: { + '&:hover': { + borderColor: '#2563EB', + boxShadow: 'inset 0 0 0 1px #2563EB', + backgroundColor: '#EFF6FF', + color: '#1E3A8A', + }, + }, +}) + +export const daoGrid = style({ + display: 'grid', + gap: '16px', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + '@media': { + 'screen and (min-width: 700px)': { + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + }, + 'screen and (min-width: 1080px)': { + gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', + }, + }, +}) + +export const daoCard = style({ + display: 'flex', + flexDirection: 'column', + gap: '18px', + minHeight: '360px', + borderRadius: '22px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '18px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + transform: 'translateY(-1px)', + borderColor: '#D7D0C0', + boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + }, + }, +}) + +export const daoTop = style({ + display: 'flex', + justifyContent: 'space-between', + gap: '12px', + alignItems: 'flex-start', +}) + +export const daoChainBadge = style({ + width: '34px', + height: '34px', + borderRadius: '12px', + border: '1px solid #DCE7F8', + background: '#FFFFFF', + boxShadow: '0 6px 14px rgba(17, 17, 17, 0.05)', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + overflow: 'hidden', +}) + +export const daoChainBadgeImage = style({ + width: '20px', + height: '20px', + objectFit: 'contain', + display: 'block', +}) + +export const daoIdentity = style({ + display: 'flex', + gap: '12px', + alignItems: 'flex-start', + minWidth: 0, +}) + +export const daoAvatar = style({ + width: '52px', + height: '52px', + borderRadius: '18px', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: 'ptRoot, sans-serif', + fontSize: '18px', + fontWeight: 700, + flexShrink: 0, + overflow: 'hidden', +}) + +export const daoAvatarImage = style({ + width: '100%', + height: '100%', + objectFit: 'cover', + display: 'block', +}) + +export const daoName = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '20px', + lineHeight: 1.05, + color: '#111111', +}) + +export const daoDescription = style({ + fontSize: '14px', + lineHeight: 1.6, + color: '#5B5649', +}) + +export const badge = style({ + display: 'inline-flex', + width: 'fit-content', + borderRadius: '999px', + border: '1px solid rgba(17, 17, 17, 0.1)', + padding: '6px 10px', + fontSize: '12px', + fontWeight: 700, +}) + +export const daoSignal = style({ + display: 'grid', + gap: '6px', + marginTop: 'auto', + padding: '14px 16px', + borderRadius: '16px', + background: '#EFF6FF', +}) + +export const daoSignalLabel = style({ + fontSize: '12px', + textTransform: 'uppercase', + letterSpacing: '0.06em', + color: '#2563EB', + fontWeight: 700, +}) + +export const daoSignalValue = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '18px', + lineHeight: 1.2, + whiteSpace: 'nowrap', + color: '#111111', +}) + +export const cardLink = style({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '10px', + color: '#1D4ED8', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + selectors: { + '&:focus-visible': focusRing, + }, +}) + +export const scrollRow = style({ + display: 'grid', + gap: '16px', + gridAutoFlow: 'column', + gridAutoColumns: 'minmax(280px, 1fr)', + overflowX: 'auto', + paddingBottom: '8px', + scrollSnapType: 'x mandatory', + selectors: { + '&::-webkit-scrollbar': { + height: '10px', + }, + '&::-webkit-scrollbar-thumb': { + background: '#D6D1C4', + borderRadius: '999px', + }, + }, + '@media': { + 'screen and (min-width: 1080px)': { + gridAutoColumns: 'minmax(280px, 320px)', + }, + }, +}) + +export const coiningCard = style({ + display: 'flex', + flexDirection: 'column', + gap: '16px', + minHeight: '360px', + scrollSnapAlign: 'start', + borderRadius: '22px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '18px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + transform: 'translateY(-1px)', + borderColor: '#D7D0C0', + boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + }, + }, +}) + +export const coiningPreview = style({ + position: 'relative', + minHeight: '220px', + borderRadius: '22px', + overflow: 'hidden', + padding: '18px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', +}) + +export const coiningPreviewTop = style({ + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'space-between', + gap: '12px', +}) + +export const coiningPreviewMark = style({ + alignSelf: 'flex-start', + display: 'inline-flex', + borderRadius: '999px', + background: 'rgba(255, 255, 255, 0.76)', + padding: '6px 10px', + fontSize: '12px', + fontWeight: 700, + color: '#111111', +}) + +export const coiningNetworkBadge = style({ + width: '34px', + height: '34px', + borderRadius: '12px', + border: '1px solid rgba(17, 17, 17, 0.08)', + background: 'rgba(255, 255, 255, 0.88)', + boxShadow: '0 6px 14px rgba(17, 17, 17, 0.05)', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + overflow: 'hidden', +}) + +export const coiningPreviewTitle = style({ + maxWidth: '10ch', + fontFamily: 'ptRoot, sans-serif', + fontSize: '32px', + lineHeight: 0.95, + color: '#111111', +}) + +export const coiningMeta = style({ + display: 'grid', + gap: '6px', + flex: 1, +}) + +export const coiningTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '22px', + lineHeight: 1.05, + color: '#111111', +}) + +export const mutedText = style({ + fontSize: '14px', + lineHeight: 1.6, + color: '#5D564A', +}) + +export const amountPill = style({ + display: 'inline-flex', + width: 'fit-content', + borderRadius: '999px', + padding: '7px 12px', + background: '#2563EB', + color: '#FFFFFF', + fontSize: '13px', + fontWeight: 700, +}) + +export const droposalList = style({ + display: 'grid', + gap: '14px', +}) + +export const droposalCard = style({ + display: 'grid', + gap: '14px', + borderRadius: '20px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '18px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + transform: 'translateY(-1px)', + borderColor: '#D7D0C0', + boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + }, + }, + '@media': { + 'screen and (min-width: 880px)': { + gridTemplateColumns: 'minmax(0, 1fr) auto', + alignItems: 'center', + }, + }, +}) + +export const droposalMeta = style({ + display: 'flex', + flexWrap: 'wrap', + gap: '10px', + alignItems: 'center', +}) + +export const statusBadge = style({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '88px', + borderRadius: '999px', + padding: '7px 12px', + fontSize: '12px', + fontWeight: 700, +}) + +export const droposalTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '24px', + lineHeight: 1.05, + color: '#111111', +}) + +export const droposalSummary = style({ + fontSize: '15px', + lineHeight: 1.65, + color: '#5A5348', + maxWidth: '72ch', +}) + +export const droposalAside = style({ + display: 'grid', + gap: '12px', + justifyItems: 'start', + '@media': { + 'screen and (min-width: 880px)': { + justifyItems: 'end', + textAlign: 'right', + }, + }, +}) + +export const stepsGrid = style({ + display: 'grid', + gap: '16px', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + '@media': { + 'screen and (min-width: 860px)': { + gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', + }, + }, +}) + +export const stepCard = style({ + minHeight: '170px', + borderRadius: '22px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '22px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', +}) + +export const stepHeader = style({ + display: 'flex', + alignItems: 'flex-start', + gap: '16px', +}) + +export const stepMarker = style({ + display: 'inline-flex', + minWidth: '48px', + height: '48px', + padding: '0 16px', + borderRadius: '18px', + alignItems: 'center', + justifyContent: 'center', + background: '#2563EB', + color: '#FFFFFF', + fontFamily: 'ptRoot, sans-serif', + fontSize: '18px', + flexShrink: 0, +}) + +export const stepTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '28px', + lineHeight: 1.02, + color: '#111111', +}) + +export const stepBody = style({ + marginTop: '14px', + fontSize: '15px', + lineHeight: 1.65, + color: '#585244', +}) + +export const valueGrid = style({ + display: 'grid', + gap: '16px', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + '@media': { + 'screen and (min-width: 700px)': { + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + }, + 'screen and (min-width: 1080px)': { + gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', + }, + }, +}) + +export const compactValueGrid = style({ + display: 'grid', + width: '100%', + maxWidth: '100%', + alignSelf: 'stretch', + gap: '16px', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + '@media': { + 'screen and (min-width: 768px)': { + gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', + }, + }, +}) + +export const useCaseGrid = style({ + display: 'grid', + width: '100%', + maxWidth: '100%', + alignSelf: 'stretch', + gap: '16px', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + '@media': { + 'screen and (min-width: 768px)': { + gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', + }, + }, +}) + +export const valueCard = style({ + minHeight: '150px', + borderRadius: '20px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '20px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', +}) + +export const compactValueCard = style({ + minHeight: '160px', + borderRadius: '20px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '20px', + boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + display: 'flex', + flexDirection: 'column', + gap: '12px', +}) + +export const valueTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '24px', + lineHeight: 1.05, + color: '#111111', +}) + +export const compactValueTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '18px', + lineHeight: 1.2, + color: '#111111', + display: '-webkit-box', + overflow: 'hidden', + WebkitLineClamp: 3, + WebkitBoxOrient: 'vertical', +}) + +export const compactValueEmoji = style({ + fontSize: '28px', + lineHeight: 1, +}) + +export const compactValueImage = style({ + width: '32px', + height: '32px', + objectFit: 'contain', + display: 'block', +}) + +export const activityList = style({ + display: 'grid', + gap: '14px', +}) + +export const activityCard = style({ + display: 'grid', + gap: '10px', + borderRadius: '20px', + border: '1px solid #E6E1D4', + background: '#FFFFFF', + padding: '18px', + '@media': { + 'screen and (min-width: 820px)': { + gridTemplateColumns: '140px minmax(0, 1fr)', + alignItems: 'start', + gap: '18px', + }, + }, +}) + +export const activityMeta = style({ + display: 'inline-flex', + width: 'fit-content', + borderRadius: '999px', + padding: '6px 10px', + fontSize: '12px', + fontWeight: 700, + background: '#F4F1E8', + color: '#575041', +}) + +export const activityTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '22px', + lineHeight: 1.08, + color: '#111111', +}) + +export const finalCta = style({ + position: 'relative', + overflow: 'hidden', + borderRadius: '26px', + border: '1px solid #BFDBFE', + background: 'linear-gradient(135deg, #1D4ED8 0%, #2563EB 58%, #3B82F6 100%)', + color: '#FFFFFF', + padding: '30px 22px', + boxShadow: '0 16px 30px rgba(17, 17, 17, 0.14)', + '@media': { + 'screen and (min-width: 768px)': { + padding: '40px 34px', + }, + }, +}) + +export const finalCtaGlow = style({ + position: 'absolute', + inset: 0, + background: + 'radial-gradient(circle at 10% 10%, rgba(191, 219, 254, 0.3), transparent 24%), radial-gradient(circle at 92% 20%, rgba(255, 255, 255, 0.16), transparent 22%)', + pointerEvents: 'none', +}) + +export const finalCtaContent = style({ + position: 'relative', + zIndex: 1, + display: 'grid', + gap: '24px', + alignItems: 'center', + '@media': { + 'screen and (min-width: 980px)': { + gridTemplateColumns: 'minmax(0, 1fr) auto', + gap: '32px', + }, + }, +}) + +export const finalCtaTitle = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '36px', + lineHeight: 0.98, + whiteSpace: 'nowrap', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '52px', + }, + }, +}) + +export const finalChecklist = style({ + marginTop: '12px', + marginBottom: 0, + padding: 0, + listStyle: 'none', + display: 'grid', + gap: '10px', +}) + +export const finalChecklistItem = style({ + display: 'flex', + alignItems: 'center', + gap: '10px', + fontSize: '17px', + lineHeight: 1.6, + color: 'rgba(255, 255, 255, 0.78)', +}) + +export const finalChecklistMarker = style({ + minWidth: '24px', + color: '#FFFFFF', + fontWeight: 700, +}) + +export const finalActions = style({ + display: 'flex', + flexDirection: 'column', + gap: '12px', + alignItems: 'stretch', + '@media': { + 'screen and (min-width: 768px)': { + flexDirection: 'row', + alignItems: 'center', + }, + 'screen and (min-width: 980px)': { + justifyContent: 'flex-end', + }, + }, +}) + +export const lightButton = style({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '42px', + padding: '10px 16px', + borderRadius: '999px', + border: '1px solid #2563EB', + background: '#2563EB', + color: '#FFFFFF', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + borderColor: '#2563EB', + boxShadow: 'inset 0 0 0 1px #2563EB', + backgroundColor: '#EFF6FF', + color: '#1E3A8A', + }, + '&:focus-visible': focusRing, + }, +}) + +export const finalPrimaryButton = style([ + lightButton, + { + border: '1px solid rgba(255, 255, 255, 0.72)', + boxShadow: + '0 0 0 1px rgba(255, 255, 255, 0.24), inset 0 0 0 1px rgba(255, 255, 255, 0.08)', + selectors: { + '&:hover': { + borderColor: '#FFFFFF', + boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.42), inset 0 0 0 1px #2563EB', + }, + }, + }, +]) + +export const ghostButton = style({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '42px', + padding: '10px 16px', + borderRadius: '999px', + background: '#FFFFFF', + color: '#1E3A8A', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + border: '1px solid #DBEAFE', + transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + selectors: { + '&:hover': { + cursor: 'pointer', + backgroundColor: '#EFF6FF', + borderColor: '#93C5FD', + }, + '&:focus-visible': focusRing, + }, +}) + +export const subLink = style({ + color: 'rgba(255, 255, 255, 0.86)', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + selectors: { + '&:hover': { + color: '#FFFFFF', + }, + '&:focus-visible': focusRing, + }, +}) + +export const heroSubLink = style({ + color: '#2563EB', + fontSize: '14px', + fontWeight: 700, + textDecoration: 'none', + selectors: { + '&:hover': { + color: '#1D4ED8', + }, + '&:focus-visible': focusRing, + }, +}) + +export const helperText = style({ + fontSize: '14px', + lineHeight: 1.55, + color: '#5B6477', +}) diff --git a/apps/web/src/modules/about/AboutPage.tsx b/apps/web/src/modules/about/AboutPage.tsx new file mode 100644 index 000000000..d7484ce80 --- /dev/null +++ b/apps/web/src/modules/about/AboutPage.tsx @@ -0,0 +1,152 @@ +import { useChainStore } from '@buildeross/stores' +import { Box } from '@buildeross/zord' +import React from 'react' + +import { + centeredImage, + centeredImageWrap, + container, + page, + section, +} from './AboutPage.css' +import { AboutFinalCta } from './components/AboutFinalCta' +import { AboutHero } from './components/AboutHero' +import { CoiningHighlightsSection } from './components/CoiningHighlightsSection' +import { DroposalHighlightsSection } from './components/DroposalHighlightsSection' +import { FeaturedDaoSection } from './components/FeaturedDaoSection' +import { ProposalHighlightsSection } from './components/ProposalHighlightsSection' +import { SectionIntro } from './components/SectionIntro' +import { WhatIsBuilderSection } from './components/WhatIsBuilderSection' +import { WhatYouCanBuildSection } from './components/WhatYouCanBuildSection' +import { WhyBuilderSection } from './components/WhyBuilderSection' +import { heroHighlights } from './data' +import { useAboutDaoTabs } from './useAboutDaoTabs' +import { useAboutShowcase } from './useAboutShowcase' +import { useAboutSnapshotStats } from './useAboutSnapshotStats' + +export const AboutPageView: React.FC = () => { + const chain = useChainStore((x) => x.chain) + const { data: tabsData, isLoading } = useAboutDaoTabs(chain.slug) + const { data: snapshotData } = useAboutSnapshotStats() + const { data: showcaseData } = useAboutShowcase() + const heroLogos = React.useMemo(() => { + if (!tabsData) return [] + + const seen = new Set() + + return Object.values(tabsData) + .flat() + .filter((dao) => dao.recentAuctionImage) + .filter((dao) => { + if (!dao.recentAuctionImage || seen.has(dao.recentAuctionImage)) return false + seen.add(dao.recentAuctionImage) + return true + }) + .slice(0, 12) + .map((dao) => ({ + id: dao.id, + name: dao.name, + imageUrl: dao.recentAuctionImage as string, + })) + }, [tabsData]) + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This public good tooling and the Nouns Builder Protocol are maintained and + governed by{' '} + + Builder DAO + + . Learn more about the DAO's vision and mission{' '} + + here + + . + + + + + + + + + + + + + + + + ) +} diff --git a/apps/web/src/modules/about/components/AboutFinalCta.tsx b/apps/web/src/modules/about/components/AboutFinalCta.tsx new file mode 100644 index 000000000..5b5887226 --- /dev/null +++ b/apps/web/src/modules/about/components/AboutFinalCta.tsx @@ -0,0 +1,56 @@ +import { Box, Text } from '@buildeross/zord' +import Link from 'next/link' +import React from 'react' + +import { + finalActions, + finalChecklist, + finalChecklistItem, + finalChecklistMarker, + finalCta, + finalCtaContent, + finalCtaGlow, + finalCtaTitle, + finalPrimaryButton, + ghostButton, + subLink, +} from '../AboutPage.css' + +export const AboutFinalCta: React.FC = () => { + return ( + + + + + Start your DAO today + + + 1. + Upload your art + + + 2. + Set your parameters + + + 3. + Launch your first auction + + + + + + + Launch your DAO + + + Explore the ecosystem + + + Read the docs + + + + + ) +} diff --git a/apps/web/src/modules/about/components/AboutHero.tsx b/apps/web/src/modules/about/components/AboutHero.tsx new file mode 100644 index 000000000..b98adf5b2 --- /dev/null +++ b/apps/web/src/modules/about/components/AboutHero.tsx @@ -0,0 +1,120 @@ +import { Box, Text } from '@buildeross/zord' +import Link from 'next/link' +import React from 'react' + +import { + hero, + heroActions, + heroCopy, + heroHighlight, + heroHighlightDot, + heroHighlightList, + heroPanel, + heroPanelGlow, + heroText, + heroTitle, + heroVennImage, + heroVennWrap, + logoMarquee, + logoMarqueeImage, + logoMarqueeInner, + logoMarqueeItem, + logoMarqueeTrack, + montageBody, + montageCard, + montageFooter, + montageGrid, + montagePrimary, + montageSide, + montageValue, + primaryButton, + secondaryButton, +} from '../AboutPage.css' + +type AboutHeroProps = { + heroHighlights: string[] + heroLogos: Array<{ + id: string + name: string + imageUrl: string + }> +} + +export const AboutHero: React.FC = ({ heroHighlights, heroLogos }) => { + const marqueeLogos = heroLogos.length > 0 ? [...heroLogos, ...heroLogos] : [] + + return ( + + + + The easiest way to build communities onchain + + + Nouns Builder lets anyone launch a DAO where membership, funding, content and + governance all happen in one fully transparent, onchain system. + + + + {heroHighlights.map((item) => ( + + + {item} + + ))} + + + + + Launch a DAO + + + + Explore existing DAOs + + + + + + + + + {`Launch.\nFund.\nCollaborate.`} + + The onchain operating system for decentralized communities. + + + + + + + + + + + + + + {marqueeLogos.map((logo, index) => ( + + + + ))} + + + + + + + + ) +} diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx new file mode 100644 index 000000000..1b10355e5 --- /dev/null +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -0,0 +1,59 @@ +import { Box, Text } from '@buildeross/zord' +import Link from 'next/link' +import React from 'react' + +import { + cardLink, + coiningCard, + coiningMeta, + coiningNetworkBadge, + coiningPreview, + coiningPreviewMark, + coiningPreviewTitle, + coiningPreviewTop, + coiningTitle, + daoChainBadgeImage, + mutedText, +} from '../AboutPage.css' +import { CoiningHighlight } from '../types' +import { getChainLogoSrc } from '../utils' + +type CoiningCardProps = { + item: CoiningHighlight +} + +export const CoiningCard: React.FC = ({ item }) => { + const chainLogoSrc = getChainLogoSrc(item.chainLabel) + + return ( + + + + {item.amount} + {chainLogoSrc ? ( + + + + ) : null} + + {item.previewLabel} + + + + {item.title} + + By {item.creator} for {item.dao} + + + + + View post + + + ) +} diff --git a/apps/web/src/modules/about/components/CoiningHighlightsSection.tsx b/apps/web/src/modules/about/components/CoiningHighlightsSection.tsx new file mode 100644 index 000000000..c3ecec7c3 --- /dev/null +++ b/apps/web/src/modules/about/components/CoiningHighlightsSection.tsx @@ -0,0 +1,34 @@ +import { Box } from '@buildeross/zord' +import React from 'react' + +import { scrollRow } from '../AboutPage.css' +import { coiningHighlights } from '../data' +import { CoiningHighlight } from '../types' +import { CoiningCard } from './CoiningCard' +import { SectionIntro } from './SectionIntro' + +type CoiningHighlightsSectionProps = { + items?: CoiningHighlight[] +} + +export const CoiningHighlightsSection: React.FC = ({ + items, +}) => { + const highlights = items?.length ? items : coiningHighlights + + return ( + + + + + {highlights.map((item) => ( + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx new file mode 100644 index 000000000..aa5fa7782 --- /dev/null +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -0,0 +1,73 @@ +import { FallbackImage } from '@buildeross/ui' +import { Box, Flex, Text } from '@buildeross/zord' +import Link from 'next/link' +import React from 'react' + +import { + cardLink, + daoAvatar, + daoAvatarImage, + daoCard, + daoChainBadge, + daoChainBadgeImage, + daoDescription, + daoIdentity, + daoName, + daoSignal, + daoSignalLabel, + daoSignalValue, + daoTop, +} from '../AboutPage.css' +import { AboutDao } from '../types' + +type DaoCardProps = { + dao: AboutDao +} + +export const DaoCard: React.FC = ({ dao }) => { + return ( + + + + + {dao.imageUrl ? ( + + ) : ( + dao.initials + )} + + + {dao.name} + {dao.description} + + + {dao.chainIcon ? ( + + + + ) : null} + + + + {dao.signalLabel} + {dao.signalValue} + + + + View DAO + + + ) +} diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx new file mode 100644 index 000000000..a2964686a --- /dev/null +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -0,0 +1,101 @@ +import { Box, Text } from '@buildeross/zord' +import Link from 'next/link' +import React from 'react' + +import { + badge, + cardLink, + daoChainBadge, + daoChainBadgeImage, + droposalAside, + droposalCard, + droposalList, + droposalMeta, + droposalSummary, + droposalTitle, + mutedText, + statusBadge, +} from '../AboutPage.css' +import { dropHighlights } from '../data' +import { DroposalHighlight } from '../types' +import { getChainLogoSrc } from '../utils' +import { SectionIntro } from './SectionIntro' + +const statusStyles: Record = { + Active: { background: '#FFF2BF', color: '#6A5300' }, + Passed: { background: '#DDF7E7', color: '#0F5B37' }, + Funded: { background: '#DCE6FF', color: '#1D3F84' }, + Executed: { background: '#ECE7FF', color: '#44348F' }, + Trending: { background: '#FFE1D7', color: '#7D2E0B' }, + Live: { background: '#DDF7E7', color: '#0F5B37' }, + Recent: { background: '#DCE6FF', color: '#1D3F84' }, +} + +type DroposalHighlightsSectionProps = { + items?: DroposalHighlight[] + eyebrowText?: string + title?: string + copy?: string + linkLabel?: string +} + +export const DroposalHighlightsSection: React.FC = ({ + items, + eyebrowText = 'Drops', + title = 'Drops turn releases into onchain distribution', + copy = 'Launch collectible drops that turn media, editions, and releases into distribution, ownership, and treasury growth for decentralized communities.', + linkLabel = 'View drop', +}) => { + const highlights = items?.length ? items : dropHighlights + + return ( + + + + + {highlights.map((proposal) => ( + + + + + {proposal.dao} + + + {proposal.status} + + {getChainLogoSrc(proposal.category) ? ( + + + + ) : ( + {proposal.category} + )} + + + {proposal.title} + + + {proposal.summary} + + + + + {proposal.amount} + + {linkLabel} + + + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/components/EcosystemActivitySection.tsx b/apps/web/src/modules/about/components/EcosystemActivitySection.tsx new file mode 100644 index 000000000..e1d9a7ac1 --- /dev/null +++ b/apps/web/src/modules/about/components/EcosystemActivitySection.tsx @@ -0,0 +1,40 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + activityCard, + activityList, + activityMeta, + activityTitle, + mutedText, +} from '../AboutPage.css' +import { ecosystemActivity } from '../data' +import { SectionIntro } from './SectionIntro' + +export const EcosystemActivitySection: React.FC = () => { + return ( + + + + + {ecosystemActivity.map((item) => ( + + {item.meta} + + + {item.title} + + + {item.detail} + + + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/components/EcosystemStatGrid.tsx b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx new file mode 100644 index 000000000..0aab29b56 --- /dev/null +++ b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx @@ -0,0 +1,33 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + statAccent, + statCard, + statDetail, + statGrid, + statLabel, + statValue, +} from '../AboutPage.css' +import { AboutStat } from '../types' + +type EcosystemStatGridProps = { + stats: AboutStat[] +} + +export const EcosystemStatGrid: React.FC = ({ stats }) => { + return ( + + {stats.map((stat) => ( + + + {stat.icon} + + {stat.label} + {stat.value} + {stat.detail} + + ))} + + ) +} diff --git a/apps/web/src/modules/about/components/FeaturedDaoSection.tsx b/apps/web/src/modules/about/components/FeaturedDaoSection.tsx new file mode 100644 index 000000000..41a0996f5 --- /dev/null +++ b/apps/web/src/modules/about/components/FeaturedDaoSection.tsx @@ -0,0 +1,82 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + activeTabButton, + daoGrid, + sectionInlineCopy, + sectionInlineRow, + sectionTitle, + sectionTitleOnly, + tabButton, + tabs, +} from '../AboutPage.css' +import { AboutDaoTabKey, AboutDaoTabsResponse, AboutStat } from '../types' +import { DaoCard } from './DaoCard' +import { EcosystemStatGrid } from './EcosystemStatGrid' + +type FeaturedDaoSectionProps = { + tabsData?: AboutDaoTabsResponse + stats?: AboutStat[] + isLoading: boolean +} + +const tabOrder: { id: AboutDaoTabKey; label: string }[] = [ + { id: 'featured', label: 'Featured' }, + { id: 'trending', label: 'Trending' }, + { id: 'new', label: 'New' }, + { id: 'active', label: 'Active' }, +] + +export const FeaturedDaoSection: React.FC = ({ + tabsData, + stats, + isLoading: _isLoading, +}) => { + const [tab, setTab] = React.useState('featured') + + const displayedDaos = React.useMemo(() => { + return tabsData?.[tab] ?? [] + }, [tab, tabsData]) + + return ( + + + A thriving ecosystem + + + + + Nouns Builder is already powering a growing network of decentralized + communities. + + + + {tabOrder.map((item) => { + const isActive = item.id === tab + return ( + + ) + })} + + + + + {displayedDaos.map((dao) => ( + + ))} + + + + + + + ) +} diff --git a/apps/web/src/modules/about/components/HowItWorksSection.tsx b/apps/web/src/modules/about/components/HowItWorksSection.tsx new file mode 100644 index 000000000..4d3d569ae --- /dev/null +++ b/apps/web/src/modules/about/components/HowItWorksSection.tsx @@ -0,0 +1,35 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + stepBody, + stepCard, + stepHeader, + stepMarker, + stepsGrid, + stepTitle, +} from '../AboutPage.css' +import { builderSteps } from '../data' +import { SectionIntro } from './SectionIntro' + +export const HowItWorksSection: React.FC = () => { + return ( + + + + + {builderSteps.map((step) => ( + + + {step.marker} + + {step.title} + + + {step.body} + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/components/ProposalHighlightsSection.tsx b/apps/web/src/modules/about/components/ProposalHighlightsSection.tsx new file mode 100644 index 000000000..b494f4a42 --- /dev/null +++ b/apps/web/src/modules/about/components/ProposalHighlightsSection.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { proposalHighlights } from '../data' +import { DroposalHighlight } from '../types' +import { DroposalHighlightsSection } from './DroposalHighlightsSection' + +type ProposalHighlightsSectionProps = { + items?: DroposalHighlight[] +} + +export const ProposalHighlightsSection: React.FC = ({ + items, +}) => { + const highlights = items?.length ? items : proposalHighlights + + return ( + + ) +} diff --git a/apps/web/src/modules/about/components/SectionIntro.tsx b/apps/web/src/modules/about/components/SectionIntro.tsx new file mode 100644 index 000000000..5aadf3d9b --- /dev/null +++ b/apps/web/src/modules/about/components/SectionIntro.tsx @@ -0,0 +1,26 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { sectionCopy, sectionHeader, sectionTitle } from '../AboutPage.css' + +type SectionIntroProps = { + eyebrowText: string + title: string + copy?: string + copyClassName?: string +} + +export const SectionIntro: React.FC = ({ + title, + copy, + copyClassName, +}) => { + return ( + + + {title} + + {copy ? {copy} : null} + + ) +} diff --git a/apps/web/src/modules/about/components/WhatIsBuilderSection.tsx b/apps/web/src/modules/about/components/WhatIsBuilderSection.tsx new file mode 100644 index 000000000..298b88146 --- /dev/null +++ b/apps/web/src/modules/about/components/WhatIsBuilderSection.tsx @@ -0,0 +1,43 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + mutedText, + stepHeader, + stepMarker, + valueCard, + valueGrid, + valueTitle, +} from '../AboutPage.css' +import { builderFeatures } from '../data' +import { SectionIntro } from './SectionIntro' + +export const WhatIsBuilderSection: React.FC = () => { + return ( + + + + + {builderFeatures.map((item) => ( + + + + {item.marker} + + + {item.title} + + + + {item.body} + + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx b/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx new file mode 100644 index 000000000..83ac218c8 --- /dev/null +++ b/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx @@ -0,0 +1,44 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + compactValueCard, + compactValueEmoji, + compactValueImage, + compactValueTitle, + useCaseGrid, +} from '../AboutPage.css' +import { builderUseCases } from '../data' +import { SectionIntro } from './SectionIntro' + +export const WhatYouCanBuildSection: React.FC = () => { + return ( + + + + + {builderUseCases.map((item) => ( + + {item.imageSrc ? ( + + ) : item.emoji ? ( + {item.emoji} + ) : null} + + {item.title} + + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/components/WhyBuilderSection.tsx b/apps/web/src/modules/about/components/WhyBuilderSection.tsx new file mode 100644 index 000000000..6fdf7cb38 --- /dev/null +++ b/apps/web/src/modules/about/components/WhyBuilderSection.tsx @@ -0,0 +1,39 @@ +import { Box, Text } from '@buildeross/zord' +import React from 'react' + +import { + compactValueCard, + compactValueGrid, + compactValueTitle, + mutedText, + section, +} from '../AboutPage.css' +import { builderValueProps } from '../data' +import { SectionIntro } from './SectionIntro' + +export const WhyBuilderSection: React.FC = () => { + return ( + + + + + {builderValueProps.map((item) => ( + + + {item.title} + + {item.body ? ( + + {item.body} + + ) : null} + + ))} + + + ) +} diff --git a/apps/web/src/modules/about/data.ts b/apps/web/src/modules/about/data.ts new file mode 100644 index 000000000..8fc39b32e --- /dev/null +++ b/apps/web/src/modules/about/data.ts @@ -0,0 +1,288 @@ +import { + ActivityItem, + BuilderFeature, + BuilderStep, + BuilderValueProp, + CoiningHighlight, + DroposalHighlight, +} from './types' + +// TODO: Replace seeded showcase data with an aggregated ecosystem endpoint once +// cross-DAO ranking and coining feeds are available for the public about page. +export const coiningHighlights: CoiningHighlight[] = [ + { + id: 'studio-coin', + title: 'Studio Coin', + creator: '0x7d4f...19ae', + dao: 'Studio Nouns', + chainLabel: 'Base', + amount: '$1.8M market cap', + href: '/explore?search=studio%20nouns', + eyebrow: 'DAO paired coin', + accent: '#7D53FF', + surface: 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)', + previewLabel: 'Creator pair', + }, + { + id: 'camp-coin', + title: 'Camp Coin', + creator: '0x4ac2...d6bf', + dao: 'Camp Nouns', + chainLabel: 'Base', + amount: '$940K market cap', + href: '/explore?search=camp%20nouns', + eyebrow: 'DAO paired coin', + accent: '#FF8A3D', + surface: 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)', + previewLabel: 'Content pair', + }, + { + id: 'builder-coin', + title: 'Builder Coin', + creator: '0x8cf4...b2da', + dao: 'BuilderDAO', + chainLabel: 'Base', + amount: '$1.2M market cap', + href: '/explore?search=builderdao', + eyebrow: 'DAO paired coin', + accent: '#0DBA80', + surface: 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)', + previewLabel: 'Onchain market', + }, + { + id: 'fest-coin', + title: 'Fest Coin', + creator: '0x1ce9...42f0', + dao: 'Nouns Fest', + chainLabel: 'Ethereum', + amount: '$680K market cap', + href: '/explore?search=nouns%20fest', + eyebrow: 'DAO paired coin', + accent: '#296BFF', + surface: 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)', + previewLabel: 'Content pair', + }, +] + +export const dropHighlights: DroposalHighlight[] = [ + { + id: 'open-edition-jam-drop', + title: 'Open Edition Jam', + dao: 'Studio Nouns', + amount: 'ETH 18.6 sold', + status: 'Live', + summary: + 'A live edition drop distributing creative work onchain with sales flowing to the DAO treasury.', + href: '/explore?search=studio%20nouns', + category: 'Base', + }, + { + id: 'summer-camp-poster-drop', + title: 'Summer Camp Poster Pack', + dao: 'Camp Nouns', + amount: 'ETH 9.4 sold', + status: 'Recent', + summary: + 'A collectible poster release used to distribute media and fund creative output through Builder.', + href: '/explore?search=camp%20nouns', + category: 'Base', + }, + { + id: 'governance-zine-drop', + title: 'Governance Zine 001', + dao: 'BuilderDAO', + amount: 'ETH 12.1 sold', + status: 'Recent', + summary: + 'An editorial drop that packages governance content into onchain ownership and treasury formation.', + href: '/explore?search=builderdao', + category: 'Base', + }, + { + id: 'signal-radio-drop', + title: 'Signal Radio Session', + dao: 'Nouns Fest', + amount: 'ETH 6.8 sold', + status: 'Live', + summary: + 'A media drop extending event programming into onchain distribution, ownership, and funding.', + href: '/explore?search=nouns%20fest', + category: 'Ethereum', + }, +] + +export const proposalHighlights: DroposalHighlight[] = [ + { + id: 'operator-tooling', + title: 'Operator tooling sprint for treasury reporting', + dao: 'BuilderDAO', + amount: '42 voters', + status: 'Funded', + summary: + 'Shared treasury analytics and operator tooling improve governance workflows across Builder DAOs.', + href: '/explore?search=builderdao', + category: 'Base', + }, + { + id: 'festival-grants', + title: 'Mini grants for local Nouns Fest activations', + dao: 'Nouns Fest', + amount: '18 voters', + status: 'Passed', + summary: + 'Regional teams get budget for activations, documentation, and post-event publishing.', + href: '/explore?search=nouns%20fest', + category: 'Ethereum', + }, + { + id: 'creator-retainer', + title: 'Quarterly creative retainer for media releases', + dao: 'Studio Nouns', + amount: '24 voters', + status: 'Active', + summary: + 'Sustains ongoing visual output with transparent milestones and release cadence.', + href: '/explore?search=studio%20nouns', + category: 'Base', + }, + { + id: 'builder-residency', + title: 'Residency round for new onchain builders', + dao: 'Far House', + amount: '30 voters', + status: 'Trending', + summary: + 'Funds a cohort of builders experimenting with coining, drops, and governance flows.', + href: '/explore?search=far%20house', + category: 'Optimism', + }, + { + id: 'archive-pipeline', + title: 'Archive and indexing pipeline for DAO memory', + dao: 'Prop House', + amount: '12 voters', + status: 'Executed', + summary: + 'Creates searchable records for proposal outcomes, experiments, and funded work.', + href: '/explore?search=prop%20house', + category: 'Ethereum', + }, +] + +export const builderSteps: BuilderStep[] = [ + { + id: 'launch', + title: 'Launch your DAO', + body: 'Spin up a DAO with auctions, governance, and treasury built in.', + marker: '1', + }, + { + id: 'grow', + title: 'Grow your treasury', + body: 'Use auctions and coining to generate ongoing funding.', + marker: '2', + }, + { + id: 'fund', + title: 'Fund ideas', + body: 'Use proposals to allocate capital to builders, events, and experiments.', + marker: '3', + }, +] + +export const builderFeatures: BuilderFeature[] = [ + { + id: 'auctions', + marker: '1', + title: 'Auctions bring in new members and fund the treasury', + body: 'Ownership and capital contribution in one simple mechanic.', + }, + { + id: 'coining', + marker: '2', + title: 'Members participate in collaborative content creation', + body: 'Create content as coins and droposals to turn media into funding utilizing built in distribution methods.', + }, + { + id: 'proposals', + marker: '3', + title: 'The treasury funds ideas', + body: 'Members vote on proposals to fund work and allocate capital, all transparently onchain.', + }, +] + +export const builderValueProps: BuilderValueProp[] = [ + { + id: 'launch-fast', + title: 'Launch a DAO in minutes', + }, + { + id: 'treasury-auctions', + title: 'Built-in treasury formation through auctions', + }, + { + id: 'governance-day-one', + title: 'Governance included from day one', + }, + { + id: 'creative-output', + title: 'Native support for creative output', + }, +] + +export const ecosystemActivity: ActivityItem[] = [ + { + id: 'launches', + title: 'New launches keep expanding the graph', + detail: + 'Fresh DAOs continue showing up with tighter scopes: media, local communities, product teams, and events.', + meta: 'Recent launches', + tone: 'launch', + }, + { + id: 'coining', + title: 'Coining is broadening what a DAO can publish', + detail: + 'Posts are becoming funding surfaces for essays, visuals, audio, and experiments instead of just announcements.', + meta: 'Creator momentum', + tone: 'coin', + }, + { + id: 'droposals', + title: 'Droposals are routing treasury into clearer outcomes', + detail: + 'Teams are packaging work with tighter scope, deliverables, and faster governance cycles.', + meta: 'Governance patterns', + tone: 'governance', + }, + { + id: 'protocol', + title: 'Builder keeps shipping new primitives', + detail: + 'The protocol is evolving around creation flows, treasury tooling, content, and governance ergonomics.', + meta: 'Protocol motion', + tone: 'protocol', + }, +] + +export const heroHighlights = [ + 'Each new member joins through an auction.', + 'Every auction funds a shared treasury.', + 'Token holders propose and vote on how that capital is used.', +] + +export const builderUseCases: BuilderValueProp[] = [ + { + id: 'media-collectives', + title: 'Community owned brands', + imageSrc: '/noggles-square.png', + }, + { id: 'creative-communities', title: 'Creative and media collectives', emoji: '🎨' }, + { id: 'events-local-groups', title: 'Local and event-based groups', emoji: '🎪' }, + { id: 'protocol-teams', title: 'Protocol teams', emoji: '⚙️' }, + { + id: 'experimental-governance', + title: 'Subculture communities', + emoji: '🛹', + }, +] diff --git a/apps/web/src/modules/about/index.ts b/apps/web/src/modules/about/index.ts new file mode 100644 index 000000000..3564b71bf --- /dev/null +++ b/apps/web/src/modules/about/index.ts @@ -0,0 +1 @@ +export * from './AboutPage' diff --git a/apps/web/src/modules/about/types.ts b/apps/web/src/modules/about/types.ts new file mode 100644 index 000000000..c03a73a5b --- /dev/null +++ b/apps/web/src/modules/about/types.ts @@ -0,0 +1,111 @@ +export type AboutStat = { + id: string + label: string + value: string + detail: string + accent: string + icon: string +} + +export type DaoCategory = 'featured' | 'trending' | 'new' | 'active' + +export type AboutDao = { + id: string + name: string + description: string + signalLabel: string + signalValue: string + href: string + badge: string + surface: string + textAccent: string + initials: string + imageUrl?: string | null + recentAuctionImage?: string | null + chainIcon?: string | null + chainName?: string | null + category: DaoCategory +} + +export type CoiningHighlight = { + id: string + title: string + creator: string + dao: string + chainLabel: string + amount: string + href: string + eyebrow: string + accent: string + surface: string + previewLabel: string +} + +export type DroposalStatus = + | 'Active' + | 'Passed' + | 'Funded' + | 'Executed' + | 'Trending' + | 'Live' + | 'Recent' + +export type DroposalHighlight = { + id: string + title: string + dao: string + amount: string + status: DroposalStatus + summary: string + href: string + category: string +} + +export type BuilderStep = { + id: string + title: string + body: string + marker: string +} + +export type BuilderValueProp = { + id: string + title: string + body?: string + emoji?: string + imageSrc?: string +} + +export type ActivityItem = { + id: string + title: string + detail: string + meta: string + tone: 'launch' | 'coin' | 'governance' | 'protocol' +} + +export type AboutDaoTabKey = 'featured' | 'trending' | 'new' | 'active' + +export type AboutDaoTabsResponse = { + featured: AboutDao[] + trending: AboutDao[] + new: AboutDao[] + active: AboutDao[] +} + +export type AboutSnapshotResponse = { + stats: AboutStat[] +} + +export type AboutShowcaseResponse = { + coining: CoiningHighlight[] + drops: DroposalHighlight[] + proposals: DroposalHighlight[] +} + +export type BuilderFeature = { + id: string + marker: string + title: string + body: string +} diff --git a/apps/web/src/modules/about/useAboutDaoTabs.ts b/apps/web/src/modules/about/useAboutDaoTabs.ts new file mode 100644 index 000000000..7b3ad8559 --- /dev/null +++ b/apps/web/src/modules/about/useAboutDaoTabs.ts @@ -0,0 +1,47 @@ +import { SWR_KEYS } from '@buildeross/constants/swrKeys' +import useSWR from 'swr' + +import { AboutDaoTabsResponse } from './types' + +type HttpError = Error & { status?: number; body?: unknown } + +const fetcher = async ( + [, network]: readonly [string, string], + { signal }: { signal?: AbortSignal } = {} +): Promise => { + const response = await fetch(`/api/about/dao-tabs?network=${network}`, { + signal, + headers: { Accept: 'application/json' }, + }) + + const text = await response.text() + const body = text ? JSON.parse(text) : {} + + if (!response.ok) { + const err: HttpError = new Error(body?.error || response.statusText) + err.status = response.status + err.body = body + throw err + } + + return body as AboutDaoTabsResponse +} + +export const useAboutDaoTabs = (network?: string) => { + const swrKey = network ? ([SWR_KEYS.EXPLORE, network] as const) : null + + const { data, error, isLoading, isValidating } = useSWR< + AboutDaoTabsResponse, + HttpError + >(swrKey, fetcher, { + revalidateOnFocus: false, + revalidateOnReconnect: true, + dedupingInterval: 60000, + }) + + return { + data, + error, + isLoading: !!swrKey && (isLoading || isValidating), + } +} diff --git a/apps/web/src/modules/about/useAboutShowcase.ts b/apps/web/src/modules/about/useAboutShowcase.ts new file mode 100644 index 000000000..a92849c61 --- /dev/null +++ b/apps/web/src/modules/about/useAboutShowcase.ts @@ -0,0 +1,44 @@ +import useSWR from 'swr' + +import { AboutShowcaseResponse } from './types' + +type HttpError = Error & { status?: number; body?: unknown } + +const fetcher = async ( + [url]: readonly [string], + { signal }: { signal?: AbortSignal } = {} +): Promise => { + const response = await fetch(url, { + signal, + headers: { Accept: 'application/json' }, + }) + + const text = await response.text() + const body = text ? JSON.parse(text) : {} + + if (!response.ok) { + const err: HttpError = new Error(body?.error || response.statusText) + err.status = response.status + err.body = body + throw err + } + + return body as AboutShowcaseResponse +} + +export const useAboutShowcase = () => { + const { data, error, isLoading, isValidating } = useSWR< + AboutShowcaseResponse, + HttpError + >(['/api/about/showcase'] as const, fetcher, { + revalidateOnFocus: false, + revalidateOnReconnect: true, + dedupingInterval: 60000, + }) + + return { + data, + error, + isLoading: isLoading || isValidating, + } +} diff --git a/apps/web/src/modules/about/useAboutSnapshotStats.ts b/apps/web/src/modules/about/useAboutSnapshotStats.ts new file mode 100644 index 000000000..06d28a026 --- /dev/null +++ b/apps/web/src/modules/about/useAboutSnapshotStats.ts @@ -0,0 +1,44 @@ +import useSWR from 'swr' + +import { AboutSnapshotResponse } from './types' + +type HttpError = Error & { status?: number; body?: unknown } + +const fetcher = async ( + [url]: readonly [string], + { signal }: { signal?: AbortSignal } = {} +): Promise => { + const response = await fetch(url, { + signal, + headers: { Accept: 'application/json' }, + }) + + const text = await response.text() + const body = text ? JSON.parse(text) : {} + + if (!response.ok) { + const err: HttpError = new Error(body?.error || response.statusText) + err.status = response.status + err.body = body + throw err + } + + return body as AboutSnapshotResponse +} + +export const useAboutSnapshotStats = () => { + const { data, error, isLoading, isValidating } = useSWR< + AboutSnapshotResponse, + HttpError + >(['/api/about/snapshot'] as const, fetcher, { + revalidateOnFocus: false, + revalidateOnReconnect: true, + dedupingInterval: 60000, + }) + + return { + data, + error, + isLoading: isLoading || isValidating, + } +} diff --git a/apps/web/src/modules/about/utils.ts b/apps/web/src/modules/about/utils.ts new file mode 100644 index 000000000..af9e443bf --- /dev/null +++ b/apps/web/src/modules/about/utils.ts @@ -0,0 +1,16 @@ +export const getChainLogoSrc = (chainName?: string | null) => { + const normalized = chainName?.trim().toLowerCase() + + switch (normalized) { + case 'base': + return '/chains/base.svg' + case 'ethereum': + return '/chains/ethereum.svg' + case 'optimism': + return '/chains/optimism.svg' + case 'zora': + return '/chains/zora.png' + default: + return null + } +} diff --git a/apps/web/src/modules/dashboard/Dashboard.tsx b/apps/web/src/modules/dashboard/Dashboard.tsx index 39fab8b6c..548747f6a 100644 --- a/apps/web/src/modules/dashboard/Dashboard.tsx +++ b/apps/web/src/modules/dashboard/Dashboard.tsx @@ -130,7 +130,9 @@ export const Dashboard: React.FC = () => { dao.proposals.some( (proposal) => proposal.state === ProposalState.Active && - !proposal.votes.some((vote) => vote.voter === address.toLowerCase()) + !proposal.votes.some( + (vote: { voter: string }) => vote.voter === address.toLowerCase() + ) ) ) }, [sortedDaos, address]) diff --git a/apps/web/src/modules/dashboard/SingleDaoSelector.tsx b/apps/web/src/modules/dashboard/SingleDaoSelector.tsx index 20ac9a96a..2ce2bd70e 100644 --- a/apps/web/src/modules/dashboard/SingleDaoSelector.tsx +++ b/apps/web/src/modules/dashboard/SingleDaoSelector.tsx @@ -1,6 +1,7 @@ import { COIN_SUPPORTED_CHAIN_IDS, PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants' import { SearchInput } from '@buildeross/feed-ui' import { useDaoSearch, useDaosWithClankerTokens, useUserDaos } from '@buildeross/hooks' +import type { MyDaosResponse } from '@buildeross/sdk/subgraph' import type { AddressType, CHAIN_ID, @@ -40,6 +41,7 @@ export interface SingleDaoSelectorProps { } const MIN_SEARCH_LENGTH = 3 +type MemberDao = MyDaosResponse[number] export const SingleDaoSelector: React.FC = ({ chainIds, @@ -60,7 +62,7 @@ export const SingleDaoSelector: React.FC = ({ // Convert MyDaosResponse to DaoListItem format (before filtering) const allDaoItems: DaoListItem[] = useMemo(() => { - let items = memberDaos.map((dao) => ({ + let items: DaoListItem[] = memberDaos.map((dao: MemberDao) => ({ name: dao.name, image: dao.contractImage, address: dao.collectionAddress.toLowerCase() as AddressType, @@ -84,13 +86,13 @@ export const SingleDaoSelector: React.FC = ({ // Group DAOs by chain for checking clanker tokens const daosByChain = useMemo(() => { - const grouped: Record = {} as Record + const grouped: Partial> = {} allDaoItems.forEach((dao) => { if (isChainIdSupportedByCoining(dao.chainId)) { if (!grouped[dao.chainId]) { grouped[dao.chainId] = [] } - grouped[dao.chainId].push(dao) + grouped[dao.chainId]!.push(dao) } }) return grouped @@ -98,11 +100,11 @@ export const SingleDaoSelector: React.FC = ({ // Fetch clanker tokens for each chain const chainQueries = COIN_SUPPORTED_CHAIN_IDS.map((chainId) => { - const daosOnChain = daosByChain[chainId] || [] + const daosOnChain: DaoListItem[] = daosByChain[chainId as CHAIN_ID] || [] // eslint-disable-next-line react-hooks/rules-of-hooks return useDaosWithClankerTokens({ chainId, - daoAddresses: daosOnChain.map((dao) => dao.address), + daoAddresses: daosOnChain.map((dao: DaoListItem) => dao.address), enabled: actionType === 'post' && daosOnChain.length > 0, }) }) diff --git a/apps/web/src/pages/about.tsx b/apps/web/src/pages/about.tsx index 2445f14c8..bc2f09451 100644 --- a/apps/web/src/pages/about.tsx +++ b/apps/web/src/pages/about.tsx @@ -1,84 +1,16 @@ -import { useChainStore } from '@buildeross/stores' -import { ContractButton } from '@buildeross/ui/ContractButton' -import { Box, Stack } from '@buildeross/zord' import Head from 'next/head' -import Image from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/router' -import React, { useCallback } from 'react' import { getDefaultLayout } from 'src/layouts/DefaultLayout' -import { whyCreateButton, whyTextStyle } from 'src/styles/about.css' +import { AboutPageView } from 'src/modules/about' import { NextPageWithLayout } from './_app' const AboutPage: NextPageWithLayout = () => { - const { push } = useRouter() - const { id: chainId } = useChainStore((x) => x.chain) - - const handleCreateClick = useCallback(() => { - push('/create') - }, [push]) - return ( <> Nouns Builder | About - - - - why - - - Nouns Builder makes it easy for communities and collectives to create Nounish - DAOs, fully equipped with onchain governance and membership auctions starting - day one. - - - This public good DAO tooling and the Nouns Builder Protocol are maintained and - governed by   - - BuilderDAO - - . Learn more about the DAO's vision and mission   - - here - - . - - - - Create a DAO - - - + ) } diff --git a/apps/web/src/pages/api/about/dao-tabs.ts b/apps/web/src/pages/api/about/dao-tabs.ts new file mode 100644 index 000000000..80e81a5b5 --- /dev/null +++ b/apps/web/src/pages/api/about/dao-tabs.ts @@ -0,0 +1,563 @@ +import { CACHE_TIMES } from '@buildeross/constants/cacheTimes' +import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains' +import { PUBLIC_SUBGRAPH_URL } from '@buildeross/constants/subgraph' +import { Auction_OrderBy, exploreDaosRequest } from '@buildeross/sdk/subgraph' +import { CHAIN_ID } from '@buildeross/types' +import { chainIdToSlug } from '@buildeross/utils/chains' +import { gql, GraphQLClient } from 'graphql-request' +import type { NextApiRequest, NextApiResponse } from 'next' +import { summarizeDaoDescription } from 'src/utils/api/ai/summaries' +import { withCors } from 'src/utils/api/cors' +import { withRateLimit } from 'src/utils/api/rateLimit' +import { parseEther } from 'viem' + +import type { AboutDao, AboutDaoTabsResponse } from '../../../modules/about/types' + +const MIN_AUCTION_SALES = parseEther('0.001').toString() +const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30 + +type DaoQueryItem = { + id: string + name: string + description: string + tokenAddress: string + contractImage?: string | null + totalSupply: number + totalAuctionSales: string + latestToken: Array<{ name: string; image?: string | null; tokenId: string }> + firstToken: Array<{ mintedAt: string }> + recentAuctions: Array<{ + id: string + endTime: string + winningBid?: { amount: string } | null + }> + currentAuction?: { + endTime: string + highestBid?: { amount: string } | null + token?: { tokenId: string; name: string; image?: string | null } | null + } | null +} + +type DaoQueryResponse = { + daos: DaoQueryItem[] +} + +type CrossChainDaoCandidate = { + chainId: CHAIN_ID + dao: DaoQueryItem +} + +type CrossChainActiveDao = { + chainId: CHAIN_ID + dao: NonNullable>>['daos'][number] +} + +type FeaturedDaoConfigItem = { + name: string + aliases?: readonly string[] + description: string + signalLabel: string + signalValue: string + fallbackChainId: CHAIN_ID +} + +const ABOUT_DAO_TABS_QUERY = gql` + query AboutDaoTabs( + $daoFirst: Int! + $daoWhere: DAO_filter + $recentAuctionWhere: Auction_filter + $recentAuctionFirst: Int! + ) { + daos( + first: $daoFirst + where: $daoWhere + orderBy: totalAuctionSales + orderDirection: desc + ) { + id + name + description + tokenAddress + contractImage + totalSupply + totalAuctionSales + latestToken: tokens(first: 1, orderBy: mintedAt, orderDirection: desc) { + name + image + tokenId + } + firstToken: tokens(first: 1, orderBy: mintedAt, orderDirection: asc) { + mintedAt + } + currentAuction { + endTime + highestBid { + amount + } + token { + tokenId + name + image + } + } + recentAuctions: auctions( + first: $recentAuctionFirst + orderBy: endTime + orderDirection: desc + where: $recentAuctionWhere + ) { + id + endTime + winningBid { + amount + } + } + } + } +` + +const buildDaoHref = (chainId: CHAIN_ID, tokenAddress: string) => + `/dao/${chainIdToSlug(chainId)}/${tokenAddress}` + +const getInitials = (name: string) => + name + .split(' ') + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() ?? '') + .join('') + +const getSurface = (index: number) => + ['#EFF6FF', '#DBEAFE', '#BFDBFE', '#E0F2FE'][index % 4] + +const FEATURED_DAO_CONFIG: readonly FeaturedDaoConfigItem[] = [ + { + name: 'Builder DAO', + aliases: ['BuilderDAO'], + description: 'Stewards Builder upgrades, ecosystem tooling, and protocol operations.', + signalLabel: 'Role', + signalValue: 'Protocol steward', + fallbackChainId: CHAIN_ID.BASE, + }, + { + name: 'Gnars DAO', + aliases: ['Gnars'], + description: 'Funds skate, surf, and snow culture through auctions and governance.', + signalLabel: 'Focus', + signalValue: 'Sports culture', + fallbackChainId: CHAIN_ID.BASE, + }, + { + name: 'Collective Nouns', + description: 'Explores shared ownership and member-led treasury deployment.', + signalLabel: 'Focus', + signalValue: 'Art collective', + fallbackChainId: CHAIN_ID.BASE, + }, + { + name: 'Nouns DAO Africa', + description: 'Supports regional builders and creatives using Nounish governance.', + signalLabel: 'Focus', + signalValue: 'Regional empowerment', + fallbackChainId: CHAIN_ID.ETHEREUM, + }, +] as const + +const chainById = new Map( + PUBLIC_DEFAULT_CHAINS.map((chain) => [chain.id, chain] as const) +) + +const normalizeDaoName = (name: string) => + name + .trim() + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + +const isNameMatch = (targets: string[], candidateName?: string | null) => { + const candidate = normalizeDaoName(candidateName || '') + + if (!candidate) return false + + return targets.some((target) => { + const normalizedTarget = normalizeDaoName(target) + + return ( + candidate === normalizedTarget || + candidate.includes(normalizedTarget) || + normalizedTarget.includes(candidate) + ) + }) +} + +const shortenDescription = (description?: string | null) => { + const base = description?.trim() + + if (!base) return null + + const [firstSentence] = base.split(/(?<=[.!?])\s+/) + const concise = firstSentence || base + + return concise.length > 110 ? `${concise.slice(0, 107).trimEnd()}...` : concise +} + +const createDaoDescriptionResolver = () => { + const summaries = new Map>() + const aiConfigured = Boolean( + process.env.AI_GATEWAY_API_KEY || process.env.OPENAI_API_KEY + ) + + return (description?: string | null) => { + const base = description?.trim() + + if (!base) { + return Promise.resolve(null) + } + + if (!aiConfigured) { + return Promise.resolve(shortenDescription(base)) + } + + if (!summaries.has(base)) { + summaries.set( + base, + summarizeDaoDescription(base) + .then((summary) => summary || shortenDescription(base)) + .catch(() => shortenDescription(base)) + ) + } + + return summaries.get(base)! + } +} + +const mapDao = ( + dao: DaoQueryItem, + chainId: CHAIN_ID, + index: number, + signalLabel: string, + signalValue: string, + badge: string, + category: AboutDao['category'], + descriptionOverride?: string | null +): AboutDao => ({ + id: `${dao.id}-${badge.toLowerCase()}`, + name: dao.name || 'Untitled DAO', + description: + descriptionOverride || + shortenDescription(dao.description) || + 'Live Builder DAO with recent auction and member activity.', + signalLabel, + signalValue, + href: buildDaoHref(chainId, dao.tokenAddress), + badge, + surface: getSurface(index), + textAccent: '#1E3A8A', + initials: getInitials(dao.name || 'DAO'), + imageUrl: dao.contractImage || dao.latestToken[0]?.image || null, + recentAuctionImage: + dao.currentAuction?.token?.image || dao.latestToken[0]?.image || null, + chainIcon: chainById.get(chainId)?.icon || null, + chainName: chainById.get(chainId)?.name || null, + category, +}) + +const fetchDaoCandidates = async ( + chainId: CHAIN_ID, + options: { totalAuctionSalesGt?: string; first?: number; nameContains?: string } = {} +): Promise => { + const subgraphUrl = PUBLIC_SUBGRAPH_URL.get(chainId) + + if (!subgraphUrl) { + throw new Error(`No subgraph URL configured for chain ${chainId}`) + } + + const client = new GraphQLClient(subgraphUrl, { + headers: { 'Content-Type': 'application/json' }, + }) + + const now = Math.floor(Date.now() / 1000) + + const data = await client.request(ABOUT_DAO_TABS_QUERY, { + daoFirst: options.first ?? 150, + daoWhere: { + totalSupply_gt: 0, + ...(options.nameContains ? { name_contains_nocase: options.nameContains } : {}), + ...(options.totalAuctionSalesGt + ? { totalAuctionSales_gt: options.totalAuctionSalesGt } + : {}), + }, + recentAuctionFirst: 40, + recentAuctionWhere: { + settled: true, + endTime_gt: now - THIRTY_DAYS_IN_SECONDS, + }, + }) + + return data.daos ?? [] +} + +const fetchCrossChainDaoCandidates = async ( + options: { totalAuctionSalesGt?: string; first?: number } = {} +): Promise => { + const results = await Promise.all( + PUBLIC_DEFAULT_CHAINS.map(async (chain) => { + const daos = await fetchDaoCandidates(chain.id, options) + + return daos.map((dao) => ({ chainId: chain.id, dao })) + }) + ) + + return results.flat() +} + +const findFeaturedMatch = (targets: string[], candidates: CrossChainDaoCandidate[]) => + candidates.find((item) => isNameMatch(targets, item.dao.name)) + +const fetchFeaturedMatch = async (targets: string[]) => { + const results = await Promise.all( + PUBLIC_DEFAULT_CHAINS.flatMap((chain) => + targets.map(async (target) => { + const daos = await fetchDaoCandidates(chain.id, { + first: 25, + nameContains: target, + }) + + return daos.map((dao) => ({ chainId: chain.id, dao })) + }) + ) + ) + + return findFeaturedMatch(targets, results.flat()) +} + +const buildFeaturedDaos = async ( + resolveDescription: ReturnType +): Promise => { + const results = await Promise.all( + PUBLIC_DEFAULT_CHAINS.map(async (chain) => { + const daos = await fetchDaoCandidates(chain.id, { first: 800 }) + + return daos.map((dao) => ({ chainId: chain.id, dao })) + }) + ) + + const featuredCandidates = results.flat() + + return Promise.all( + FEATURED_DAO_CONFIG.map(async (item, index) => { + const targets = [item.name, ...(item.aliases ?? [])] + const match = + findFeaturedMatch(targets, featuredCandidates) || + (await fetchFeaturedMatch(targets)) + + if (!match) { + return { + id: `${normalizeDaoName(item.name).replace(/\s+/g, '-')}-featured`, + name: item.name, + description: item.description, + signalLabel: item.signalLabel, + signalValue: item.signalValue, + href: '/explore', + badge: 'Featured', + surface: getSurface(index), + textAccent: '#1E3A8A', + initials: getInitials(item.name), + imageUrl: null, + recentAuctionImage: null, + chainIcon: chainById.get(item.fallbackChainId)?.icon || null, + chainName: chainById.get(item.fallbackChainId)?.name || null, + category: 'featured' as const, + } + } + + const description = + (await resolveDescription(match.dao.description || item.description)) || + shortenDescription(match.dao.description || item.description) + + return { + ...mapDao( + { + ...match.dao, + description: match.dao.description || item.description, + }, + match.chainId, + index, + item.signalLabel, + item.signalValue, + 'Featured', + 'featured', + description + ), + name: item.name, + } + }) + ) +} + +const buildTrendingDaos = async ( + items: CrossChainDaoCandidate[], + resolveDescription: ReturnType +) => + Promise.all( + items + .map((item) => ({ + ...item, + recentSalesCount: item.dao.recentAuctions.length, + recentVolume: item.dao.recentAuctions.reduce( + (total, auction) => total + BigInt(auction.winningBid?.amount ?? '0'), + 0n + ), + })) + .filter((item) => item.recentSalesCount > 0) + .sort((a, b) => { + if (b.recentSalesCount !== a.recentSalesCount) { + return b.recentSalesCount - a.recentSalesCount + } + if (b.recentVolume === a.recentVolume) { + return 0 + } + return b.recentVolume > a.recentVolume ? 1 : -1 + }) + .slice(0, 4) + .map(async (item, index) => + mapDao( + item.dao, + item.chainId, + index, + 'Sales in 30d', + `${item.recentSalesCount} settled`, + 'Trending', + 'trending', + (await resolveDescription(item.dao.description)) || + shortenDescription(item.dao.description) + ) + ) + ) + +const buildNewDaos = async ( + items: CrossChainDaoCandidate[], + resolveDescription: ReturnType +) => + Promise.all( + items + .filter((item) => item.dao.totalSupply > 3 && item.dao.firstToken[0]?.mintedAt) + .sort( + (a, b) => + Number(b.dao.firstToken[0].mintedAt) - Number(a.dao.firstToken[0].mintedAt) + ) + .slice(0, 4) + .map(async (item, index) => + mapDao( + item.dao, + item.chainId, + index, + 'Supply sold', + `${item.dao.totalSupply} tokens`, + 'New', + 'new', + (await resolveDescription(item.dao.description)) || + shortenDescription(item.dao.description) + ) + ) + ) + +const buildActiveDaos = async ( + daoCandidates: CrossChainDaoCandidate[], + resolveDescription: ReturnType +) => { + const now = Math.floor(Date.now() / 1000) + const activeResults = await Promise.all( + PUBLIC_DEFAULT_CHAINS.map(async (chain) => { + const result = await exploreDaosRequest(chain.id, 12, 0, Auction_OrderBy.EndTime) + + return (result?.daos ?? []).map((dao) => ({ + chainId: chain.id, + dao, + })) + }) + ) + + const activeDaos = activeResults.flat() as CrossChainActiveDao[] + + return Promise.all( + activeDaos + .filter((item) => item.dao.endTime && Number(item.dao.endTime) > now) + .sort((a, b) => Number(a.dao.endTime) - Number(b.dao.endTime)) + .slice(0, 4) + .map(async (item, index) => { + const matchingDao = daoCandidates.find( + (candidate) => + candidate.chainId === item.chainId && + candidate.dao.tokenAddress.toLowerCase() === + item.dao.dao.tokenAddress.toLowerCase() + ) + + const descriptionSource = matchingDao?.dao.description + const description = + (await resolveDescription(descriptionSource)) || + shortenDescription(descriptionSource) || + 'Live Builder DAO with recent auction and member activity.' + + return { + id: `${item.chainId}-${item.dao.dao.tokenAddress}-active`, + name: item.dao.dao.name || item.dao.token?.name || 'Untitled DAO', + description, + signalLabel: 'Ends soon', + signalValue: `${Math.max(1, Math.round((Number(item.dao.endTime) - now) / 3600))}h left`, + href: buildDaoHref(item.chainId, item.dao.dao.tokenAddress), + badge: 'Active', + surface: getSurface(index), + textAccent: '#1E3A8A', + initials: getInitials(item.dao.dao.name || item.dao.token?.name || 'DAO'), + imageUrl: matchingDao?.dao.contractImage || item.dao.token?.image || null, + recentAuctionImage: item.dao.token?.image || null, + chainIcon: chainById.get(item.chainId)?.icon || null, + chainName: chainById.get(item.chainId)?.name || null, + category: 'active' as const, + } + }) + ) +} + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + try { + const resolveDescription = createDaoDescriptionResolver() + + const [featuredDaos, daoCandidates] = await Promise.all([ + buildFeaturedDaos(resolveDescription), + fetchCrossChainDaoCandidates({ + totalAuctionSalesGt: MIN_AUCTION_SALES, + first: 150, + }), + ]) + + const payload: AboutDaoTabsResponse = { + featured: featuredDaos, + trending: await buildTrendingDaos(daoCandidates, resolveDescription), + new: await buildNewDaos(daoCandidates, resolveDescription), + active: await buildActiveDaos(daoCandidates, resolveDescription), + } + + const { maxAge, swr } = CACHE_TIMES.EXPLORE + res.setHeader( + 'Cache-Control', + `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}` + ) + + return res.status(200).json(payload) + } catch (error) { + console.error('About dao tabs error:', error) + return res.status(500).json({ error: 'Failed to load about page DAO tabs' }) + } +} + +export default withCors()( + withRateLimit({ + maxRequests: 60, + windowSeconds: 60, + keyPrefix: 'about:dao-tabs', + })(handler) +) diff --git a/apps/web/src/pages/api/about/showcase.ts b/apps/web/src/pages/api/about/showcase.ts new file mode 100644 index 000000000..9f9a4b952 --- /dev/null +++ b/apps/web/src/pages/api/about/showcase.ts @@ -0,0 +1,396 @@ +import { CACHE_TIMES } from '@buildeross/constants/cacheTimes' +import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains' +import { PUBLIC_SUBGRAPH_URL } from '@buildeross/constants/subgraph' +import { CHAIN_ID } from '@buildeross/types' +import { DEFAULT_ZORA_TOTAL_SUPPLY, walletSnippet } from '@buildeross/utils' +import { chainIdToSlug } from '@buildeross/utils/chains' +import { gql, GraphQLClient } from 'graphql-request' +import type { NextApiRequest, NextApiResponse } from 'next' +import { getZoraCoinUsdPrice } from 'src/services/coinPriceService' +import { withCors } from 'src/utils/api/cors' +import { withRateLimit } from 'src/utils/api/rateLimit' +import { formatEther } from 'viem' + +import { + coiningHighlights as fallbackCoiningHighlights, + dropHighlights as fallbackDropHighlights, + proposalHighlights as fallbackProposalHighlights, +} from '../../../modules/about/data' +import type { + AboutShowcaseResponse, + CoiningHighlight, + DroposalHighlight, + DroposalStatus, +} from '../../../modules/about/types' + +type ShowcaseZoraDrop = { + id: string + name: string + description?: string | null + imageURI?: string | null + animationURI?: string | null + creator: string + totalSalesAmount: string + createdAt: string + dao?: { + name: string + tokenAddress: string + } | null +} + +type ShowcaseZoraCoin = { + id: string + coinAddress: string + name: string + symbol: string + caller: string + createdAt: string + dao?: { + name: string + tokenAddress: string + } | null +} + +type ShowcaseProposal = { + id: string + proposalId: string + proposalNumber: number + title?: string | null + description?: string | null + timeCreated: string + voteEnd: string + voteCount: number + queued: boolean + executed: boolean + canceled: boolean + vetoed: boolean + dao: { + name: string + tokenAddress: string + } +} + +type ShowcaseQueryResponse = { + zoraCoins: ShowcaseZoraCoin[] + zoraDrops: ShowcaseZoraDrop[] + proposals: ShowcaseProposal[] +} + +type CrossChainCoin = ShowcaseZoraCoin & { chainId: CHAIN_ID } +type CrossChainDrop = ShowcaseZoraDrop & { chainId: CHAIN_ID } +type CrossChainProposal = ShowcaseProposal & { chainId: CHAIN_ID } + +const ABOUT_SHOWCASE_QUERY = gql` + query AboutShowcase($coinFirst: Int!, $dropFirst: Int!, $proposalFirst: Int!) { + zoraCoins( + first: $coinFirst + orderBy: createdAt + orderDirection: desc + where: { dao_not: null } + ) { + id + coinAddress + name + symbol + caller + createdAt + dao { + name + tokenAddress + } + } + zoraDrops( + first: $dropFirst + orderBy: totalSalesAmount + orderDirection: desc + where: { dao_not: null, totalSalesAmount_gt: 0 } + ) { + id + name + description + imageURI + animationURI + creator + totalSalesAmount + createdAt + dao { + name + tokenAddress + } + } + proposals( + first: $proposalFirst + orderBy: timeCreated + orderDirection: desc + where: { title_not: null } + ) { + id + proposalId + proposalNumber + title + description + timeCreated + voteEnd + voteCount + queued + executed + canceled + vetoed + dao { + name + tokenAddress + } + } + } +` + +const previewSurfaces = [ + 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)', + 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)', + 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)', + 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)', +] + +const compactVotes = (votes: number) => + `${new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, + }).format(votes)} voters` + +const compactMarketCap = (marketCapUsd: number) => + `$${new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, + }).format(marketCapUsd)} market cap` + +const compactEthAmount = (amountInEth: number) => + `ETH ${new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, + }).format(amountInEth)} sold` + +const cleanSentence = (value?: string | null) => { + const base = value + ?.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ') + ?.replace(/\[[^\]]+\]\([^)]+\)/g, ' ') + ?.replace(/[`*_>#-]/g, ' ') + ?.replace(/\s+/g, ' ') + ?.trim() + + if (!base) return null + + const [firstSentence] = base.split(/(?<=[.!?])\s+/) + const concise = firstSentence || base + + return concise.length > 120 ? `${concise.slice(0, 117).trimEnd()}...` : concise +} + +const deriveProposalStatus = ( + proposal: ShowcaseProposal, + now: number +): DroposalStatus | null => { + if (proposal.canceled || proposal.vetoed) return null + if (proposal.executed) return 'Executed' + if (proposal.queued) return 'Funded' + if (Number(proposal.voteEnd) > now) return 'Active' + return 'Passed' +} + +const fetchShowcaseDataForChain = async ( + chainId: CHAIN_ID +): Promise<{ + coins: CrossChainCoin[] + drops: CrossChainDrop[] + proposals: CrossChainProposal[] +}> => { + const subgraphUrl = PUBLIC_SUBGRAPH_URL.get(chainId) + + if (!subgraphUrl) { + return { coins: [], drops: [], proposals: [] } + } + + const client = new GraphQLClient(subgraphUrl, { + headers: { 'Content-Type': 'application/json' }, + }) + + const data = await client.request(ABOUT_SHOWCASE_QUERY, { + coinFirst: 30, + dropFirst: 12, + proposalFirst: 20, + }) + + return { + coins: (data.zoraCoins ?? []).map((coin) => ({ ...coin, chainId })), + drops: (data.zoraDrops ?? []).map((drop) => ({ ...drop, chainId })), + proposals: (data.proposals ?? []).map((proposal) => ({ ...proposal, chainId })), + } +} + +const buildCoiningHighlights = async ( + coins: CrossChainCoin[] +): Promise => { + const coinsWithMarketCap = await Promise.all( + coins + .filter((coin) => coin.dao?.tokenAddress && coin.coinAddress && coin.name) + .map(async (coin) => { + const priceUsd = await getZoraCoinUsdPrice(coin.coinAddress, coin.chainId).catch( + () => null + ) + + const marketCapUsd = + priceUsd && Number.isFinite(priceUsd) + ? priceUsd * DEFAULT_ZORA_TOTAL_SUPPLY + : null + + return { + coin, + marketCapUsd, + } + }) + ) + + return coinsWithMarketCap + .filter( + ( + item + ): item is { + coin: CrossChainCoin + marketCapUsd: number + } => item.marketCapUsd !== null && item.marketCapUsd > 0 + ) + .sort((a, b) => b.marketCapUsd - a.marketCapUsd) + .slice(0, 4) + .map(({ coin, marketCapUsd }, index) => ({ + id: `${coin.chainId}-${coin.id}`, + title: coin.name, + creator: walletSnippet(coin.caller as `0x${string}`), + dao: coin.dao?.name || 'Builder DAO', + chainLabel: + PUBLIC_DEFAULT_CHAINS.find((chain) => chain.id === coin.chainId)?.name || 'Base', + amount: compactMarketCap(marketCapUsd), + href: `/coin/${chainIdToSlug(coin.chainId)}/${coin.coinAddress}`, + eyebrow: 'DAO paired coin', + accent: '#2563EB', + surface: previewSurfaces[index % previewSurfaces.length], + previewLabel: coin.symbol || 'Content coin', + })) +} + +const buildDropHighlights = (drops: CrossChainDrop[]): DroposalHighlight[] => { + const now = Math.floor(Date.now() / 1000) + + return drops + .filter((drop) => drop.dao?.tokenAddress && drop.name) + .sort((a, b) => Number(b.totalSalesAmount || '0') - Number(a.totalSalesAmount || '0')) + .slice(0, 5) + .map((drop) => ({ + id: `${drop.chainId}-${drop.id}`, + title: drop.name, + dao: drop.dao?.name || 'Builder DAO', + amount: compactEthAmount(Number(formatEther(BigInt(drop.totalSalesAmount || '0')))), + status: Number(drop.createdAt) > now - 60 * 60 * 24 * 14 ? 'Live' : 'Recent', + summary: + cleanSentence(drop.description) || + 'A live onchain drop distributing content through Builder.', + href: `/drop/${chainIdToSlug(drop.chainId)}/${drop.id}`, + category: + PUBLIC_DEFAULT_CHAINS.find((chain) => chain.id === drop.chainId)?.name || + 'Onchain drop', + })) +} + +const buildProposalHighlights = ( + proposals: CrossChainProposal[] +): DroposalHighlight[] => { + const now = Math.floor(Date.now() / 1000) + + return proposals + .map((proposal) => ({ + proposal, + status: deriveProposalStatus(proposal, now), + })) + .filter( + ( + item + ): item is { + proposal: CrossChainProposal + status: DroposalStatus + } => Boolean(item.status) + ) + .sort((a, b) => { + if (a.status === 'Active' && b.status !== 'Active') return -1 + if (a.status !== 'Active' && b.status === 'Active') return 1 + return Number(b.proposal.timeCreated) - Number(a.proposal.timeCreated) + }) + .slice(0, 5) + .map(({ proposal, status }) => ({ + id: `${proposal.chainId}-${proposal.id}`, + title: proposal.title?.trim() || `Proposal #${proposal.proposalNumber}`, + dao: proposal.dao.name, + amount: compactVotes(proposal.voteCount || 0), + status, + summary: + cleanSentence(proposal.description) || + `Proposal #${proposal.proposalNumber} in ${proposal.dao.name}.`, + href: `/dao/${chainIdToSlug(proposal.chainId)}/${proposal.dao.tokenAddress}/vote/${proposal.proposalId}`, + category: + PUBLIC_DEFAULT_CHAINS.find((chain) => chain.id === proposal.chainId)?.name || + 'DAO proposal', + })) +} + +const buildShowcaseResponse = async (): Promise => { + const chainData = await Promise.all( + PUBLIC_DEFAULT_CHAINS.map((chain) => fetchShowcaseDataForChain(chain.id)) + ) + + const coins = chainData.flatMap((item) => item.coins) + const drops = chainData.flatMap((item) => item.drops) + const proposals = chainData.flatMap((item) => item.proposals) + + return { + coining: await buildCoiningHighlights(coins), + drops: buildDropHighlights(drops), + proposals: buildProposalHighlights(proposals), + } +} + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + try { + const payload = await buildShowcaseResponse() + const { maxAge, swr } = CACHE_TIMES.EXPLORE + + res.setHeader( + 'Cache-Control', + `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}` + ) + + return res.status(200).json({ + coining: payload.coining.length ? payload.coining : fallbackCoiningHighlights, + drops: payload.drops.length ? payload.drops : fallbackDropHighlights, + proposals: payload.proposals.length + ? payload.proposals + : fallbackProposalHighlights, + }) + } catch (error) { + console.error('About showcase error:', error) + + return res.status(200).json({ + coining: fallbackCoiningHighlights, + drops: fallbackDropHighlights, + proposals: fallbackProposalHighlights, + }) + } +} + +export default withCors()( + withRateLimit({ + maxRequests: 60, + windowSeconds: 60, + keyPrefix: 'about:showcase', + })(handler) +) diff --git a/apps/web/src/pages/api/about/snapshot.ts b/apps/web/src/pages/api/about/snapshot.ts new file mode 100644 index 000000000..ee270aaa9 --- /dev/null +++ b/apps/web/src/pages/api/about/snapshot.ts @@ -0,0 +1,264 @@ +import { CACHE_TIMES } from '@buildeross/constants/cacheTimes' +import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains' +import { PUBLIC_SUBGRAPH_URL } from '@buildeross/constants/subgraph' +import { gql, GraphQLClient } from 'graphql-request' +import type { NextApiRequest, NextApiResponse } from 'next' +import { withCors } from 'src/utils/api/cors' +import { withRateLimit } from 'src/utils/api/rateLimit' +import { formatEther } from 'viem' + +import type { AboutSnapshotResponse } from '../../../modules/about/types' + +type SnapshotDao = { + id: string + totalAuctionSales: string + proposalCount: number + ownerCount: number + totalSupply: number + currentAuction?: { + id: string + endTime: string + settled: boolean + } | null +} + +type SnapshotQueryResponse = { + daos: SnapshotDao[] +} + +type SnapshotOwner = { + owner: string +} + +type SnapshotOwnersQueryResponse = { + daoTokenOwners: SnapshotOwner[] +} + +const ABOUT_SNAPSHOT_QUERY = gql` + query AboutSnapshot($first: Int!, $where: DAO_filter) { + daos(first: $first, where: $where, orderBy: totalAuctionSales, orderDirection: desc) { + id + totalAuctionSales + proposalCount + ownerCount + totalSupply + currentAuction { + id + endTime + settled + } + } + } +` + +const ABOUT_SNAPSHOT_OWNERS_QUERY = gql` + query AboutSnapshotOwners($first: Int!, $skip: Int!) { + daoTokenOwners(first: $first, skip: $skip) { + owner + } + } +` + +const compactNumber = (value: number) => + new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: value >= 1000 ? 1 : 0, + }).format(value) + +const compactEth = (value: number) => + `ETH ${new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, + }).format(value)}` + +const OWNER_PAGE_SIZE = 1000 + +const fetchChainSnapshot = async (subgraphUrl: string): Promise => { + const client = new GraphQLClient(subgraphUrl, { + headers: { 'Content-Type': 'application/json' }, + }) + + const data = await client.request(ABOUT_SNAPSHOT_QUERY, { + first: 1000, + where: { + totalSupply_gt: 0, + }, + }) + + return data.daos ?? [] +} + +const fetchUniqueOwnersForChain = async (subgraphUrl: string): Promise> => { + const client = new GraphQLClient(subgraphUrl, { + headers: { 'Content-Type': 'application/json' }, + }) + + const owners = new Set() + let skip = 0 + + while (true) { + const data = await client.request( + ABOUT_SNAPSHOT_OWNERS_QUERY, + { + first: OWNER_PAGE_SIZE, + skip, + } + ) + + const page = data.daoTokenOwners ?? [] + + for (const item of page) { + if (item.owner) { + owners.add(item.owner.toLowerCase()) + } + } + + if (page.length < OWNER_PAGE_SIZE) { + break + } + + skip += OWNER_PAGE_SIZE + } + + return owners +} + +const buildSnapshotResponse = async (): Promise => { + const now = Math.floor(Date.now() / 1000) + const chainSnapshots = await Promise.all( + PUBLIC_DEFAULT_CHAINS.map(async (chain) => { + const subgraphUrl = PUBLIC_SUBGRAPH_URL.get(chain.id) + if (!subgraphUrl) { + return { daos: [], uniqueOwners: null as Set | null } + } + + const [daos, uniqueOwners] = await Promise.all([ + fetchChainSnapshot(subgraphUrl), + fetchUniqueOwnersForChain(subgraphUrl).catch((error) => { + console.error( + `About snapshot unique owner fetch failed for chain ${chain.id}:`, + error + ) + return null + }), + ]) + + return { daos, uniqueOwners } + }) + ) + + const daos = chainSnapshots.flatMap((snapshot) => snapshot.daos) + + const totalDaos = daos.length + const totalAuctionSales = daos.reduce((total, dao) => { + const sales = Number(formatEther(BigInt(dao.totalAuctionSales || '0'))) + return total + sales + }, 0) + const activeAuctions = daos.filter( + (dao) => + dao.currentAuction && + !dao.currentAuction.settled && + Number(dao.currentAuction.endTime) > now + ).length + const totalProposals = daos.reduce( + (total, dao) => total + Number(dao.proposalCount || 0), + 0 + ) + const uniqueOwners = new Set() + + for (const snapshot of chainSnapshots) { + if (!snapshot.uniqueOwners) continue + + for (const owner of snapshot.uniqueOwners) { + uniqueOwners.add(owner) + } + } + + const totalMembers = + uniqueOwners.size || + daos.reduce((total, dao) => total + Number(dao.ownerCount || 0), 0) + const totalTokens = daos.reduce((total, dao) => total + Number(dao.totalSupply || 0), 0) + + return { + stats: [ + { + id: 'daos', + label: 'DAOs launched', + value: compactNumber(totalDaos), + detail: 'Live DAO count indexed across Builder-supported public networks.', + accent: '#2563EB', + icon: '🚀', + }, + { + id: 'treasury', + label: 'Auction value raised', + value: compactEth(totalAuctionSales), + detail: + 'Cumulative native-token auction sales flowing into community treasuries.', + accent: '#2563EB', + icon: '💰', + }, + { + id: 'auctions', + label: 'Active auctions', + value: compactNumber(activeAuctions), + detail: 'Current live auctions still accepting bids across the ecosystem.', + accent: '#2563EB', + icon: '⏰', + }, + { + id: 'proposals', + label: 'Governance proposals', + value: compactNumber(totalProposals), + detail: 'Total proposals created across DAOs using Builder governance.', + accent: '#2563EB', + icon: '📜', + }, + { + id: 'members', + label: 'Members holding tokens', + value: compactNumber(totalMembers), + detail: 'Distinct DAO token holders participating across Builder communities.', + accent: '#2563EB', + icon: '👥', + }, + { + id: 'tokens', + label: 'Tokens auctioned', + value: compactNumber(totalTokens), + detail: 'Member tokens minted and distributed through recurring auctions.', + accent: '#2563EB', + icon: '🔥', + }, + ], + } +} + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + try { + const payload = await buildSnapshotResponse() + const { maxAge, swr } = CACHE_TIMES.EXPLORE + + res.setHeader( + 'Cache-Control', + `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}` + ) + + return res.status(200).json(payload) + } catch (error) { + console.error('About snapshot error:', error) + return res.status(500).json({ error: 'Failed to load about snapshot stats' }) + } +} + +export default withCors()( + withRateLimit({ + maxRequests: 60, + windowSeconds: 60, + keyPrefix: 'about:snapshot', + })(handler) +) diff --git a/apps/web/src/pages/api/ai/generateTxSummary.ts b/apps/web/src/pages/api/ai/generateTxSummary.ts index 22e6739c9..b882c5674 100644 --- a/apps/web/src/pages/api/ai/generateTxSummary.ts +++ b/apps/web/src/pages/api/ai/generateTxSummary.ts @@ -1,4 +1,3 @@ -import { gateway } from '@ai-sdk/gateway' import { CACHE_TIMES } from '@buildeross/constants/cacheTimes' import { PUBLIC_ALL_CHAINS } from '@buildeross/constants/chains' import type { @@ -12,13 +11,9 @@ import type { DecodedEscrowData } from '@buildeross/utils/escrow' import { formatBpsValue, formatTokenValue } from '@buildeross/utils/formatArgs' import { walletSnippet } from '@buildeross/utils/helpers' import * as Sentry from '@sentry/nextjs' -import { generateText } from 'ai' import { NextApiRequest, NextApiResponse } from 'next' -import { getRedisConnection } from 'src/services/redisConnection' +import { AI_MODEL, generateCachedAiText } from 'src/utils/api/ai/summaries' import { withRateLimit } from 'src/utils/api/rateLimit' -import { keccak256, toHex } from 'viem' - -const AI_MODEL = process.env.AI_MODEL || 'openai/gpt-4-turbo' type RequestBody = { chainId: CHAIN_ID @@ -30,14 +25,6 @@ type RequestBody = { escrowData?: DecodedEscrowData } -const safeStringify = (v: unknown) => - JSON.stringify(v, (_k, val) => (typeof val === 'bigint' ? val.toString() : val)) - -const getCacheKey = (data: RequestBody, model: string) => { - const hash = keccak256(toHex(`${safeStringify(data)}:${model}`)) - return `ai:txSummary:${hash}` -} - /** * Recursively traverses transaction arguments and formats * token-related fields or BPS percentage fields. @@ -276,27 +263,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const prompt = generatePrompt(requestData) const model = AI_MODEL - const redisConnection = getRedisConnection() - const cacheKey = getCacheKey(requestData, model) - - // Check cache first - let cachedText = await redisConnection?.get(cacheKey) - - if (cachedText) { - return res.status(200).json({ text: cachedText }) - } - - // Generate new text if not in cache - const result = await generateText({ - model: gateway(model), + const text = await generateCachedAiText({ + namespace: 'ai:txSummary', + data: requestData, prompt, - abortSignal: AbortSignal.timeout(30000), + model, }) - // Cache the generated text for 30 days - await redisConnection?.setex(cacheKey, 60 * 60 * 24 * 30, result.text) - - res.status(200).json({ text: result.text }) + res.status(200).json({ text }) } catch (error) { console.error(`Error generating transaction summary:`, error) diff --git a/apps/web/src/pages/api/migrated.ts b/apps/web/src/pages/api/migrated.ts index 3a67fa899..df2589036 100644 --- a/apps/web/src/pages/api/migrated.ts +++ b/apps/web/src/pages/api/migrated.ts @@ -14,7 +14,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const data = await Promise.all( L2_CHAINS.map((chainId) => { - const deployer = L2_MIGRATION_DEPLOYER[chainId] + const deployer = + L2_MIGRATION_DEPLOYER[chainId as keyof typeof L2_MIGRATION_DEPLOYER] if (deployer === zeroAddress) return [] return readContract(serverConfig, { diff --git a/apps/web/src/pages/dao/[network]/[token]/index.tsx b/apps/web/src/pages/dao/[network]/[token]/index.tsx index 9ee149ea7..d199b67c6 100644 --- a/apps/web/src/pages/dao/[network]/[token]/index.tsx +++ b/apps/web/src/pages/dao/[network]/[token]/index.tsx @@ -43,6 +43,7 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress const { address: signerAddress } = useAccount() const { addresses } = useDaoStore() const chain = useChainStore((x) => x.chain) + const chainIdKey = chain.id as keyof typeof MERKLE_RESERVE_MINTER const auctionContractParams = { abi: auctionAbi, @@ -64,12 +65,12 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress { ...tokenContractParams, functionName: 'minter', - args: [MERKLE_RESERVE_MINTER[chain.id]], + args: [MERKLE_RESERVE_MINTER[chainIdKey]], }, { ...tokenContractParams, functionName: 'minter', - args: [ERC721_REDEEM_MINTER[chain.id]], + args: [ERC721_REDEEM_MINTER[chainIdKey]], }, ] as const, }) @@ -112,13 +113,13 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress const handleMinterEnabled = React.useCallback( async (minterAddress: AddressType) => { // Navigate to appropriate tab when minter is enabled - if (minterAddress === MERKLE_RESERVE_MINTER[chain.id]) { + if (minterAddress === MERKLE_RESERVE_MINTER[chainIdKey]) { await openTab('merkle-reserve') - } else if (minterAddress === ERC721_REDEEM_MINTER[chain.id]) { + } else if (minterAddress === ERC721_REDEEM_MINTER[chainIdKey]) { await openTab('erc721-redeem') } }, - [chain.id, openTab] + [chainIdKey, openTab] ) const openTokenPage = React.useCallback( diff --git a/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx b/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx index d24f62033..69d99e1ad 100644 --- a/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx +++ b/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx @@ -146,23 +146,23 @@ const CreateProposalPage: NextPageWithLayout = () => { const addresses = useDaoStore((x) => x.addresses) const { auction, token } = addresses const chain = useChainStore((x) => x.chain) - const transactionType = useProposalStore((x) => x.transactionType) - const setTransactionType = useProposalStore((x) => x.setTransactionType) - const resetTransactionType = useProposalStore((x) => x.resetTransactionType) - const transactions = useProposalStore((x) => x.transactions) - const title = useProposalStore((x) => x.title) - const summary = useProposalStore((x) => x.summary) - const representedAddress = useProposalStore((x) => x.representedAddress) - const discussionUrl = useProposalStore((x) => x.discussionUrl) - const representedAddressEnabled = useProposalStore((x) => x.representedAddressEnabled) - const setTitle = useProposalStore((x) => x.setTitle) - const setSummary = useProposalStore((x) => x.setSummary) - const setRepresentedAddress = useProposalStore((x) => x.setRepresentedAddress) - const setDiscussionUrl = useProposalStore((x) => x.setDiscussionUrl) - const setRepresentedAddressEnabled = useProposalStore( - (x) => x.setRepresentedAddressEnabled - ) - const clearProposal = useProposalStore((x) => x.clearProposal) + const { + transactionType, + setTransactionType, + resetTransactionType, + transactions, + title, + summary, + representedAddress, + discussionUrl, + representedAddressEnabled, + setTitle, + setSummary, + setRepresentedAddress, + setDiscussionUrl, + setRepresentedAddressEnabled, + clearProposal, + } = useProposalStore() const initialStageFromQuery = query?.stage === 'transactions' ? 'transactions' : 'draft' const [createStage, setCreateStage] = React.useState<'draft' | 'transactions'>(() => diff --git a/apps/web/src/styles/about.css.ts b/apps/web/src/styles/about.css.ts deleted file mode 100644 index d7caa55aa..000000000 --- a/apps/web/src/styles/about.css.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { globalStyle, style } from '@vanilla-extract/css' - -export const whyTextStyle = style({ - maxWidth: 720, -}) - -globalStyle(`${whyTextStyle} > *`, { - fontFamily: 'Arial Narrow, sans-serif!important', -}) - -export const whyCreateButton = style({ - selectors: { - '&:hover': { - cursor: 'pointer', - }, - }, -}) diff --git a/apps/web/src/utils/api/ai/summaries.ts b/apps/web/src/utils/api/ai/summaries.ts new file mode 100644 index 000000000..e7b063e3a --- /dev/null +++ b/apps/web/src/utils/api/ai/summaries.ts @@ -0,0 +1,84 @@ +import { gateway } from '@ai-sdk/gateway' +import { generateText } from 'ai' +import { getRedisConnection } from 'src/services/redisConnection' +import { keccak256, toHex } from 'viem' + +export const AI_MODEL = process.env.AI_MODEL || 'openai/gpt-4-turbo' +const DEFAULT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 30 + +const safeStringify = (value: unknown) => + JSON.stringify(value, (_key, item) => + typeof item === 'bigint' ? item.toString() : item + ) + +export const getAiCacheKey = ( + namespace: string, + data: unknown, + model: string = AI_MODEL +) => { + const hash = keccak256(toHex(`${safeStringify(data)}:${model}`)) + return `${namespace}:${hash}` +} + +export const generateCachedAiText = async ({ + namespace, + data, + prompt, + model = AI_MODEL, + ttlSeconds = DEFAULT_CACHE_TTL_SECONDS, +}: { + namespace: string + data: unknown + prompt: string + model?: string + ttlSeconds?: number +}) => { + const redisConnection = getRedisConnection() + const cacheKey = getAiCacheKey(namespace, data, model) + + const cachedText = await redisConnection?.get(cacheKey) + + if (cachedText) { + return cachedText + } + + const result = await generateText({ + model: gateway(model), + prompt, + abortSignal: AbortSignal.timeout(30000), + }) + + const text = result.text.trim().replace(/\s+/g, ' ') + + await redisConnection?.setex(cacheKey, ttlSeconds, text) + + return text +} + +const trimDescription = (description: string, maxChars = 2200) => + description.trim().replace(/\s+/g, ' ').slice(0, maxChars) + +const buildDaoDescriptionPrompt = ( + description: string +) => `You are writing concise directory copy for a DAO platform. +Summarize the DAO description below into 1-2 short sentences for a directory card. + +Writing Rules: +- Keep it factual, clear, and neutral. +- Do not mention the DAO name. +- Do not use hype, markdown, bullet points, or line breaks. +- Prefer concrete activity, purpose, or community focus over abstract framing. +- Maximum 2 short sentences. + +DAO description: +${trimDescription(description)} + +Final Instruction: +Respond with only the 1-2 sentence summary.` + +export const summarizeDaoDescription = async (description: string) => + generateCachedAiText({ + namespace: 'ai:daoDescription', + data: { description }, + prompt: buildDaoDescriptionPrompt(description), + }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad9d715f5..4a03ea9a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,7 +169,7 @@ importers: version: 2.2.10(@tanstack/react-query@5.90.3(react@19.2.1))(@types/react@19.2.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(wagmi@2.18.1(@tanstack/query-core@5.90.3)(@tanstack/react-query@5.90.3(react@19.2.1))(@types/react@19.2.2)(bufferutil@4.0.9)(encoding@0.1.13)(ioredis@5.8.1)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(zod@4.1.12)) '@sentry/nextjs': specifier: ^9.22.0 - version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) + version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) '@smartinvoicexyz/types': specifier: ^0.1.27 version: 0.1.28(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.1))(@types/react@19.2.2)(react@19.2.1))(@types/node@22.18.10)(@types/react@19.2.2)(bufferutil@4.0.9)(encoding@0.1.13)(framer-motion@12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.11.0)(react-dom@19.2.1(react@19.2.1))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) @@ -212,6 +212,9 @@ importers: framer-motion: specifier: ^12.12.1 version: 12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + graphql-request: + specifier: ^7.1.2 + version: 7.3.0(graphql@16.11.0) ioredis: specifier: ^5.8.1 version: 5.8.1 @@ -1339,7 +1342,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^9.22.0 - version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) + version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) graphql: specifier: ^16.11.0 version: 16.11.0 @@ -15336,10 +15339,10 @@ snapshots: '@metamask/superstruct': 3.2.1 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 debug: 4.4.3(supports-color@8.1.1) pony-cause: 2.1.11 - semver: 7.7.3 + semver: 7.7.4 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -16505,7 +16508,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1)': + '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.37.0 @@ -24667,7 +24670,7 @@ snapshots: rlp@2.2.7: dependencies: - bn.js: 5.2.2 + bn.js: 5.2.3 rollup@4.52.4: dependencies: diff --git a/turbo.json b/turbo.json index 03c1fecc0..16f0579fd 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,8 @@ "ETHERSCAN_API_KEY", "AI_GATEWAY_API_KEY", "AI_MODEL", + "OPENAI_API_KEY", + "NEYNAR_API_KEY", "NEXT_PUBLIC_DISABLE_AI_SUMMARY", "NEXT_PUBLIC_DISABLE_TENDERLY_SIMULATION", "NEXT_PUBLIC_PINATA_GATEWAY", From c2bad274a25376b4ee729e0bfc45e2484af2a729 Mon Sep 17 00:00:00 2001 From: xSatori <99294685+xSatori@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:51:02 -0400 Subject: [PATCH 02/13] Refine AboutPage responsive styles & wrapping Improve responsive behavior and fix overflow/wrapping issues across the AboutPage styles. Add overflowX: 'clip' and multiple minWidth: 0 rules to prevent horizontal overflow, enable text wrapping via whiteSpace/overflowWrap changes, and introduce media queries to restore larger sizes at desktop breakpoints. Adjust typography (font sizes and line-heights) for hero, titles, and various cards, reduce paddings/min-heights for mobile and reapply previous values at wider viewports, and tweak layout grids (montage -> single column on mobile, two-column on wider screens) and scroll/marquee sizing to use vw-based columns on small screens. Misc updates include reducing gaps/padding for compact view, updating logo marquee and tile sizes, and general polish for consistent responsive presentation. --- apps/web/src/modules/about/AboutPage.css.ts | 225 ++++++++++++++++---- 1 file changed, 178 insertions(+), 47 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index ea7481e9b..b9dbc130a 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -19,6 +19,7 @@ export const page = style({ padding: '48px 16px 96px', boxSizing: 'border-box', background: '#FFFFFF', + overflowX: 'clip', '@media': { 'screen and (min-width: 768px)': { padding: '64px 24px 120px', @@ -29,6 +30,8 @@ export const page = style({ export const container = style({ maxWidth: '1180px', margin: '0 auto', + width: '100%', + minWidth: 0, }) export const centeredImageWrap = style({ @@ -94,13 +97,16 @@ export const eyebrow = style({ export const sectionTitle = style({ fontFamily: 'ptRoot, sans-serif', - fontSize: '32px', - lineHeight: 1.05, - whiteSpace: 'nowrap', + fontSize: '28px', + lineHeight: 1.08, + whiteSpace: 'normal', + overflowWrap: 'anywhere', color: '#111111', '@media': { 'screen and (min-width: 768px)': { fontSize: '44px', + whiteSpace: 'nowrap', + overflowWrap: 'normal', }, }, }) @@ -119,7 +125,12 @@ export const introCopyNoWrap = style({ fontSize: '17px', lineHeight: 1.6, color: '#5C5648', - whiteSpace: 'nowrap', + whiteSpace: 'normal', + '@media': { + 'screen and (min-width: 960px)': { + whiteSpace: 'nowrap', + }, + }, }) export const hero = style({ @@ -139,15 +150,17 @@ export const heroCopy = style({ flexDirection: 'column', gap: '20px', paddingTop: '8px', + minWidth: 0, }) export const heroTitle = style({ fontFamily: 'ptRoot, sans-serif', - fontSize: '44px', - lineHeight: 0.98, + fontSize: '36px', + lineHeight: 1, letterSpacing: '-0.03em', color: '#111111', maxWidth: '760px', + overflowWrap: 'anywhere', '@media': { 'screen and (min-width: 768px)': { fontSize: '60px', @@ -157,9 +170,15 @@ export const heroTitle = style({ export const heroText = style({ maxWidth: '640px', - fontSize: '18px', - lineHeight: 1.65, + fontSize: '16px', + lineHeight: 1.6, color: '#4C463B', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '18px', + lineHeight: 1.65, + }, + }, }) export const heroHighlightList = style({ @@ -173,7 +192,7 @@ export const heroHighlightList = style({ export const heroHighlight = style({ display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', gap: '10px', fontSize: '15px', lineHeight: 1.5, @@ -256,9 +275,15 @@ export const heroPanel = style({ borderRadius: '28px', border: '1px solid #E6E1D4', background: '#F8FBFF', - padding: '22px', - minHeight: '420px', + padding: '14px', + minHeight: 'auto', boxShadow: '0 10px 24px rgba(17, 17, 17, 0.04)', + '@media': { + 'screen and (min-width: 768px)': { + padding: '22px', + minHeight: '420px', + }, + }, }) export const heroPanelGlow = style({ @@ -273,13 +298,13 @@ export const montageGrid = style({ position: 'relative', zIndex: 1, display: 'grid', - gap: '14px', - gridTemplateColumns: '1.05fr 0.95fr', - gridTemplateAreas: '"primary side" "footer footer"', + gap: '12px', + gridTemplateColumns: '1fr', + gridTemplateAreas: '"primary" "side" "footer"', '@media': { - 'screen and (max-width: 540px)': { - gridTemplateColumns: '1fr', - gridTemplateAreas: '"primary" "side" "footer"', + 'screen and (min-width: 541px)': { + gridTemplateColumns: '1.05fr 0.95fr', + gridTemplateAreas: '"primary side" "footer footer"', }, }, }) @@ -288,18 +313,29 @@ export const montageCard = style({ borderRadius: '20px', border: '1px solid rgba(17, 17, 17, 0.08)', background: 'rgba(255, 255, 255, 0.92)', - padding: '18px', + padding: '16px', boxShadow: '0 6px 16px rgba(17, 17, 17, 0.04)', + minWidth: 0, + '@media': { + 'screen and (min-width: 768px)': { + padding: '18px', + }, + }, }) export const montagePrimary = style({ gridArea: 'primary', - minHeight: '150px', + minHeight: 'unset', }) export const montageSide = style({ gridArea: 'side', - minHeight: '220px', + minHeight: '180px', + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '220px', + }, + }, }) export const montageSecondary = style({ @@ -309,18 +345,28 @@ export const montageSecondary = style({ export const montageFooter = style({ gridArea: 'footer', - minHeight: '180px', + minHeight: '120px', padding: '10px', + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '180px', + }, + }, }) export const logoMarquee = style({ width: '100%', height: '100%', - minHeight: '160px', + minHeight: '96px', overflow: 'hidden', borderRadius: '16px', background: 'linear-gradient(135deg, rgba(239, 246, 255, 0.9) 0%, rgba(255, 255, 255, 0.9) 100%)', + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '160px', + }, + }, }) export const logoMarqueeTrack = style({ @@ -344,18 +390,24 @@ export const logoMarqueeTrack = style({ export const logoMarqueeInner = style({ display: 'flex', alignItems: 'center', - gap: '18px', + gap: '12px', width: 'max-content', minWidth: '100%', height: '100%', - padding: '22px 0', + padding: '14px 0', animation: `${marqueeScroll} 28s linear infinite`, + '@media': { + 'screen and (min-width: 768px)': { + gap: '18px', + padding: '22px 0', + }, + }, }) export const logoMarqueeItem = style({ - width: '86px', - height: '86px', - borderRadius: '22px', + width: '64px', + height: '64px', + borderRadius: '18px', overflow: 'hidden', background: '#FFFFFF', border: '1px solid rgba(37, 99, 235, 0.08)', @@ -364,6 +416,13 @@ export const logoMarqueeItem = style({ alignItems: 'center', justifyContent: 'center', flexShrink: 0, + '@media': { + 'screen and (min-width: 768px)': { + width: '86px', + height: '86px', + borderRadius: '22px', + }, + }, }) export const logoMarqueeImage = style({ @@ -382,12 +441,17 @@ export const montageLabel = style({ }) export const montageValue = style({ - marginTop: '10px', fontFamily: 'ptRoot, sans-serif', - fontSize: '28px', + fontSize: '24px', lineHeight: 1, whiteSpace: 'pre-line', color: '#111111', + '@media': { + 'screen and (min-width: 768px)': { + marginTop: '10px', + fontSize: '28px', + }, + }, }) export const montageBody = style({ @@ -500,9 +564,14 @@ export const statValue = style({ zIndex: 1, marginTop: '18px', fontFamily: 'ptRoot, sans-serif', - fontSize: '42px', + fontSize: '36px', lineHeight: 1, color: '#111111', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '42px', + }, + }, }) export const statDetail = style({ @@ -611,12 +680,13 @@ export const daoCard = style({ display: 'flex', flexDirection: 'column', gap: '18px', - minHeight: '360px', + minHeight: '320px', borderRadius: '22px', border: '1px solid #E6E1D4', background: '#FFFFFF', padding: '18px', boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + minWidth: 0, transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', selectors: { '&:hover': { @@ -625,6 +695,11 @@ export const daoCard = style({ boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', }, }, + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '360px', + }, + }, }) export const daoTop = style({ @@ -688,6 +763,7 @@ export const daoName = style({ fontSize: '20px', lineHeight: 1.05, color: '#111111', + overflowWrap: 'anywhere', }) export const daoDescription = style({ @@ -727,7 +803,8 @@ export const daoSignalValue = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '18px', lineHeight: 1.2, - whiteSpace: 'nowrap', + whiteSpace: 'normal', + overflowWrap: 'anywhere', color: '#111111', }) @@ -749,7 +826,7 @@ export const scrollRow = style({ display: 'grid', gap: '16px', gridAutoFlow: 'column', - gridAutoColumns: 'minmax(280px, 1fr)', + gridAutoColumns: 'minmax(84vw, 1fr)', overflowX: 'auto', paddingBottom: '8px', scrollSnapType: 'x mandatory', @@ -763,6 +840,9 @@ export const scrollRow = style({ }, }, '@media': { + 'screen and (min-width: 640px)': { + gridAutoColumns: 'minmax(280px, 1fr)', + }, 'screen and (min-width: 1080px)': { gridAutoColumns: 'minmax(280px, 320px)', }, @@ -773,13 +853,14 @@ export const coiningCard = style({ display: 'flex', flexDirection: 'column', gap: '16px', - minHeight: '360px', + minHeight: '320px', scrollSnapAlign: 'start', borderRadius: '22px', border: '1px solid #E6E1D4', background: '#FFFFFF', padding: '18px', boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + minWidth: 0, transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', selectors: { '&:hover': { @@ -788,17 +869,27 @@ export const coiningCard = style({ boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', }, }, + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '360px', + }, + }, }) export const coiningPreview = style({ position: 'relative', - minHeight: '220px', + minHeight: '180px', borderRadius: '22px', overflow: 'hidden', padding: '18px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '220px', + }, + }, }) export const coiningPreviewTop = style({ @@ -836,9 +927,15 @@ export const coiningNetworkBadge = style({ export const coiningPreviewTitle = style({ maxWidth: '10ch', fontFamily: 'ptRoot, sans-serif', - fontSize: '32px', + fontSize: '26px', lineHeight: 0.95, color: '#111111', + overflowWrap: 'anywhere', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '32px', + }, + }, }) export const coiningMeta = style({ @@ -920,9 +1017,15 @@ export const statusBadge = style({ export const droposalTitle = style({ fontFamily: 'ptRoot, sans-serif', - fontSize: '24px', + fontSize: '20px', lineHeight: 1.05, color: '#111111', + overflowWrap: 'anywhere', + '@media': { + 'screen and (min-width: 768px)': { + fontSize: '24px', + }, + }, }) export const droposalSummary = style({ @@ -1021,7 +1124,10 @@ export const compactValueGrid = style({ gap: '16px', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', '@media': { - 'screen and (min-width: 768px)': { + 'screen and (min-width: 700px)': { + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + }, + 'screen and (min-width: 1080px)': { gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', }, }, @@ -1035,7 +1141,13 @@ export const useCaseGrid = style({ gap: '16px', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', '@media': { - 'screen and (min-width: 768px)': { + 'screen and (min-width: 640px)': { + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + }, + 'screen and (min-width: 900px)': { + gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', + }, + 'screen and (min-width: 1180px)': { gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', }, }, @@ -1051,15 +1163,22 @@ export const valueCard = style({ }) export const compactValueCard = style({ - minHeight: '160px', + minHeight: '136px', borderRadius: '20px', border: '1px solid #E6E1D4', background: '#FFFFFF', - padding: '20px', + padding: '18px', boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', display: 'flex', flexDirection: 'column', gap: '12px', + minWidth: 0, + '@media': { + 'screen and (min-width: 768px)': { + minHeight: '160px', + padding: '20px', + }, + }, }) export const valueTitle = style({ @@ -1161,6 +1280,7 @@ export const finalCtaContent = style({ display: 'grid', gap: '24px', alignItems: 'center', + minWidth: 0, '@media': { 'screen and (min-width: 980px)': { gridTemplateColumns: 'minmax(0, 1fr) auto', @@ -1171,12 +1291,15 @@ export const finalCtaContent = style({ export const finalCtaTitle = style({ fontFamily: 'ptRoot, sans-serif', - fontSize: '36px', - lineHeight: 0.98, - whiteSpace: 'nowrap', + fontSize: '32px', + lineHeight: 1, + whiteSpace: 'normal', + overflowWrap: 'anywhere', '@media': { 'screen and (min-width: 768px)': { fontSize: '52px', + whiteSpace: 'nowrap', + overflowWrap: 'normal', }, }, }) @@ -1192,11 +1315,18 @@ export const finalChecklist = style({ export const finalChecklistItem = style({ display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', gap: '10px', - fontSize: '17px', - lineHeight: 1.6, + fontSize: '15px', + lineHeight: 1.55, color: 'rgba(255, 255, 255, 0.78)', + '@media': { + 'screen and (min-width: 768px)': { + alignItems: 'center', + fontSize: '17px', + lineHeight: 1.6, + }, + }, }) export const finalChecklistMarker = style({ @@ -1290,6 +1420,7 @@ export const subLink = style({ fontSize: '14px', fontWeight: 700, textDecoration: 'none', + width: 'fit-content', selectors: { '&:hover': { color: '#FFFFFF', From 6c5978c37dca0f645fdbeb8bcff83ec7d19531db Mon Sep 17 00:00:00 2001 From: xSatori <99294685+xSatori@users.noreply.github.com> Date: Wed, 6 May 2026 17:04:11 -0400 Subject: [PATCH 03/13] Apply a design system that conforms with the rest of the site Aligns this styling with the broader design system on the app --- apps/web/src/modules/about/AboutPage.css.ts | 364 +++++++++--------- .../components/DroposalHighlightsSection.tsx | 5 +- apps/web/src/modules/about/data.ts | 6 +- apps/web/src/modules/about/types.ts | 5 +- apps/web/src/pages/api/about/showcase.ts | 15 +- 5 files changed, 199 insertions(+), 196 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index b9dbc130a..96bd179ca 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -1,10 +1,14 @@ +import { color, theme } from '@buildeross/zord' import { keyframes, style } from '@vanilla-extract/css' const focusRing = { - outline: '3px solid rgba(37, 99, 235, 0.28)', - outlineOffset: '3px', + outline: `2px solid ${theme.colors.border}`, + outlineOffset: '2px', } +const standardBorder = '2px solid #111111' +const standardBorderThin = '1px solid #111111' + const marqueeScroll = keyframes({ '0%': { transform: 'translateX(0)', @@ -18,7 +22,7 @@ export const page = style({ width: '100%', padding: '48px 16px 96px', boxSizing: 'border-box', - background: '#FFFFFF', + background: color.background1, overflowX: 'clip', '@media': { 'screen and (min-width: 768px)': { @@ -84,27 +88,27 @@ export const eyebrow = style({ display: 'inline-flex', width: 'fit-content', borderRadius: '999px', - padding: '6px 12px', - background: '#FFFFFF', - border: '1px solid #E6E3D7', + padding: '4px 10px', + background: color.background2, + border: standardBorderThin, fontSize: '12px', lineHeight: '16px', - letterSpacing: '0.08em', + letterSpacing: '0.04em', textTransform: 'uppercase', - color: '#2563EB', + color: '#666666', fontWeight: 700, }) export const sectionTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '28px', - lineHeight: 1.08, + lineHeight: 1.04, whiteSpace: 'normal', overflowWrap: 'anywhere', color: '#111111', '@media': { 'screen and (min-width: 768px)': { - fontSize: '44px', + fontSize: '40px', whiteSpace: 'nowrap', overflowWrap: 'normal', }, @@ -116,15 +120,15 @@ export const sectionTitleOnly = style({ }) export const sectionCopy = style({ - fontSize: '17px', - lineHeight: 1.6, - color: '#5C5648', + fontSize: '16px', + lineHeight: 1.55, + color: '#666666', }) export const introCopyNoWrap = style({ - fontSize: '17px', - lineHeight: 1.6, - color: '#5C5648', + fontSize: '16px', + lineHeight: 1.55, + color: '#666666', whiteSpace: 'normal', '@media': { 'screen and (min-width: 960px)': { @@ -163,7 +167,7 @@ export const heroTitle = style({ overflowWrap: 'anywhere', '@media': { 'screen and (min-width: 768px)': { - fontSize: '60px', + fontSize: '52px', }, }, }) @@ -172,7 +176,7 @@ export const heroText = style({ maxWidth: '640px', fontSize: '16px', lineHeight: 1.6, - color: '#4C463B', + color: '#666666', '@media': { 'screen and (min-width: 768px)': { fontSize: '18px', @@ -194,17 +198,18 @@ export const heroHighlight = style({ display: 'flex', alignItems: 'flex-start', gap: '10px', - fontSize: '15px', + fontSize: '14px', lineHeight: 1.5, - color: '#232018', + color: '#444444', }) export const heroHighlightDot = style({ - width: '10px', - height: '10px', + width: '8px', + height: '8px', borderRadius: '999px', - background: '#2563EB', + background: '#111111', flexShrink: 0, + marginTop: '6px', }) export const primaryButton = style({ @@ -214,19 +219,18 @@ export const primaryButton = style({ minHeight: '42px', padding: '10px 16px', borderRadius: '999px', - border: '1px solid #2563EB', + border: standardBorderThin, color: '#FFFFFF', - background: '#2563EB', + background: '#111111', fontSize: '14px', fontWeight: 700, textDecoration: 'none', - transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { - borderColor: '#2563EB', - boxShadow: 'inset 0 0 0 1px #2563EB', - backgroundColor: '#EFF6FF', - color: '#1E3A8A', + borderColor: '#111111', + backgroundColor: '#333333', + color: '#FFFFFF', }, '&:focus-visible': focusRing, }, @@ -252,18 +256,18 @@ export const secondaryButton = style({ minHeight: '42px', padding: '10px 16px', borderRadius: '999px', - border: '1px solid #DBEAFE', - color: '#1E3A8A', - background: '#FFFFFF', + border: standardBorderThin, + color: '#111111', + background: color.background1, fontSize: '14px', fontWeight: 700, textDecoration: 'none', - transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { cursor: 'pointer', - backgroundColor: '#EFF6FF', - borderColor: '#93C5FD', + backgroundColor: color.background2, + borderColor: theme.colors.neutralHover, }, '&:focus-visible': focusRing, }, @@ -272,15 +276,15 @@ export const secondaryButton = style({ export const heroPanel = style({ position: 'relative', overflow: 'hidden', - borderRadius: '28px', - border: '1px solid #E6E1D4', - background: '#F8FBFF', + borderRadius: '16px', + border: standardBorder, + background: color.background2, padding: '14px', minHeight: 'auto', - boxShadow: '0 10px 24px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', '@media': { 'screen and (min-width: 768px)': { - padding: '22px', + padding: '16px', minHeight: '420px', }, }, @@ -289,9 +293,8 @@ export const heroPanel = style({ export const heroPanelGlow = style({ position: 'absolute', inset: 0, - background: - 'radial-gradient(circle at 20% 20%, rgba(147, 197, 253, 0.18), transparent 24%), radial-gradient(circle at 80% 10%, rgba(191, 219, 254, 0.18), transparent 18%)', pointerEvents: 'none', + background: 'transparent', }) export const montageGrid = style({ @@ -310,17 +313,12 @@ export const montageGrid = style({ }) export const montageCard = style({ - borderRadius: '20px', - border: '1px solid rgba(17, 17, 17, 0.08)', - background: 'rgba(255, 255, 255, 0.92)', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '16px', - boxShadow: '0 6px 16px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', minWidth: 0, - '@media': { - 'screen and (min-width: 768px)': { - padding: '18px', - }, - }, }) export const montagePrimary = style({ @@ -359,9 +357,8 @@ export const logoMarquee = style({ height: '100%', minHeight: '96px', overflow: 'hidden', - borderRadius: '16px', - background: - 'linear-gradient(135deg, rgba(239, 246, 255, 0.9) 0%, rgba(255, 255, 255, 0.9) 100%)', + borderRadius: '10px', + background: color.background2, '@media': { 'screen and (min-width: 768px)': { minHeight: '160px', @@ -380,7 +377,7 @@ export const logoMarqueeTrack = style({ position: 'absolute', inset: 0, background: - 'linear-gradient(90deg, rgba(248, 251, 255, 1) 0%, rgba(248, 251, 255, 0) 10%, rgba(248, 251, 255, 0) 90%, rgba(248, 251, 255, 1) 100%)', + 'linear-gradient(90deg, rgba(242, 242, 242, 1) 0%, rgba(242, 242, 242, 0) 10%, rgba(242, 242, 242, 0) 90%, rgba(242, 242, 242, 1) 100%)', zIndex: 2, pointerEvents: 'none', }, @@ -407,11 +404,11 @@ export const logoMarqueeInner = style({ export const logoMarqueeItem = style({ width: '64px', height: '64px', - borderRadius: '18px', + borderRadius: '12px', overflow: 'hidden', - background: '#FFFFFF', - border: '1px solid rgba(37, 99, 235, 0.08)', - boxShadow: '0 10px 22px rgba(17, 17, 17, 0.06)', + background: color.background1, + border: standardBorderThin, + boxShadow: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', @@ -436,7 +433,7 @@ export const montageLabel = style({ fontSize: '12px', letterSpacing: '0.08em', textTransform: 'uppercase', - color: '#726A5B', + color: '#808080', fontWeight: 700, }) @@ -458,7 +455,7 @@ export const montageBody = style({ marginTop: '10px', fontSize: '14px', lineHeight: 1.55, - color: '#5C5648', + color: '#666666', }) export const daoMiniList = style({ @@ -519,17 +516,16 @@ export const statCard = style({ position: 'relative', overflow: 'hidden', minHeight: '178px', - borderRadius: '20px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '22px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', - transition: 'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease', + boxShadow: 'none', + transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - transform: 'translateY(-1px)', - borderColor: '#D7D0C0', - boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + borderColor: theme.colors.neutralHover, + backgroundColor: color.background2, }, }, }) @@ -540,8 +536,8 @@ export const statAccent = style({ right: '18px', width: '54px', height: '54px', - borderRadius: '18px', - opacity: 0.9, + borderRadius: '12px', + opacity: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', @@ -556,7 +552,7 @@ export const statLabel = style({ fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', - color: '#6F685A', + color: '#808080', }) export const statValue = style({ @@ -616,9 +612,9 @@ export const sectionInlineRow = style({ export const sectionInlineCopy = style({ maxWidth: '760px', - fontSize: '17px', - lineHeight: 1.6, - color: '#5C5648', + fontSize: '16px', + lineHeight: 1.55, + color: '#666666', margin: 0, }) @@ -631,33 +627,32 @@ export const tabs = style({ export const tabButton = style({ minHeight: '42px', borderRadius: '999px', - border: '1px solid #DBEAFE', - background: '#FFFFFF', + border: standardBorderThin, + background: color.background1, padding: '10px 16px', fontSize: '14px', fontWeight: 700, - color: '#1E3A8A', - transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + color: '#111111', + transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { cursor: 'pointer', - borderColor: '#93C5FD', - backgroundColor: '#EFF6FF', + borderColor: theme.colors.neutralHover, + backgroundColor: color.background2, }, '&:focus-visible': focusRing, }, }) export const activeTabButton = style({ - background: '#2563EB', - borderColor: '#2563EB', - color: '#FFFFFF', + background: color.background2, + borderColor: theme.colors.neutralHover, + color: '#111111', selectors: { '&:hover': { - borderColor: '#2563EB', - boxShadow: 'inset 0 0 0 1px #2563EB', - backgroundColor: '#EFF6FF', - color: '#1E3A8A', + borderColor: theme.colors.neutralHover, + backgroundColor: color.background2, + color: '#111111', }, }, }) @@ -681,18 +676,17 @@ export const daoCard = style({ flexDirection: 'column', gap: '18px', minHeight: '320px', - borderRadius: '22px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '18px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', minWidth: 0, - transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', + transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - transform: 'translateY(-1px)', - borderColor: '#D7D0C0', - boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + borderColor: theme.colors.neutralHover, + backgroundColor: color.background2, }, }, '@media': { @@ -712,10 +706,10 @@ export const daoTop = style({ export const daoChainBadge = style({ width: '34px', height: '34px', - borderRadius: '12px', - border: '1px solid #DCE7F8', - background: '#FFFFFF', - boxShadow: '0 6px 14px rgba(17, 17, 17, 0.05)', + borderRadius: '10px', + border: standardBorderThin, + background: color.background1, + boxShadow: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -769,14 +763,14 @@ export const daoName = style({ export const daoDescription = style({ fontSize: '14px', lineHeight: 1.6, - color: '#5B5649', + color: '#666666', }) export const badge = style({ display: 'inline-flex', width: 'fit-content', borderRadius: '999px', - border: '1px solid rgba(17, 17, 17, 0.1)', + border: standardBorderThin, padding: '6px 10px', fontSize: '12px', fontWeight: 700, @@ -787,15 +781,15 @@ export const daoSignal = style({ gap: '6px', marginTop: 'auto', padding: '14px 16px', - borderRadius: '16px', - background: '#EFF6FF', + borderRadius: '10px', + background: color.background2, }) export const daoSignalLabel = style({ fontSize: '12px', textTransform: 'uppercase', letterSpacing: '0.06em', - color: '#2563EB', + color: '#808080', fontWeight: 700, }) @@ -813,11 +807,14 @@ export const cardLink = style({ alignItems: 'center', justifyContent: 'space-between', gap: '10px', - color: '#1D4ED8', + color: '#111111', fontSize: '14px', fontWeight: 700, textDecoration: 'none', selectors: { + '&:hover': { + opacity: 0.7, + }, '&:focus-visible': focusRing, }, }) @@ -855,18 +852,17 @@ export const coiningCard = style({ gap: '16px', minHeight: '320px', scrollSnapAlign: 'start', - borderRadius: '22px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '18px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', minWidth: 0, - transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', + transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - transform: 'translateY(-1px)', - borderColor: '#D7D0C0', - boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + borderColor: theme.colors.neutralHover, + backgroundColor: color.background2, }, }, '@media': { @@ -879,12 +875,13 @@ export const coiningCard = style({ export const coiningPreview = style({ position: 'relative', minHeight: '180px', - borderRadius: '22px', + borderRadius: '10px', overflow: 'hidden', padding: '18px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', + border: standardBorderThin, '@media': { 'screen and (min-width: 768px)': { minHeight: '220px', @@ -903,7 +900,8 @@ export const coiningPreviewMark = style({ alignSelf: 'flex-start', display: 'inline-flex', borderRadius: '999px', - background: 'rgba(255, 255, 255, 0.76)', + background: color.background1, + border: standardBorderThin, padding: '6px 10px', fontSize: '12px', fontWeight: 700, @@ -913,10 +911,10 @@ export const coiningPreviewMark = style({ export const coiningNetworkBadge = style({ width: '34px', height: '34px', - borderRadius: '12px', - border: '1px solid rgba(17, 17, 17, 0.08)', - background: 'rgba(255, 255, 255, 0.88)', - boxShadow: '0 6px 14px rgba(17, 17, 17, 0.05)', + borderRadius: '10px', + border: standardBorderThin, + background: color.background1, + boxShadow: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -954,7 +952,7 @@ export const coiningTitle = style({ export const mutedText = style({ fontSize: '14px', lineHeight: 1.6, - color: '#5D564A', + color: '#666666', }) export const amountPill = style({ @@ -962,8 +960,9 @@ export const amountPill = style({ width: 'fit-content', borderRadius: '999px', padding: '7px 12px', - background: '#2563EB', - color: '#FFFFFF', + background: color.background2, + color: '#111111', + border: standardBorderThin, fontSize: '13px', fontWeight: 700, }) @@ -976,17 +975,16 @@ export const droposalList = style({ export const droposalCard = style({ display: 'grid', gap: '14px', - borderRadius: '20px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '18px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', - transition: 'transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease', + boxShadow: 'none', + transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - transform: 'translateY(-1px)', - borderColor: '#D7D0C0', - boxShadow: '0 10px 22px rgba(17, 17, 17, 0.05)', + borderColor: theme.colors.neutralHover, + backgroundColor: color.background2, }, }, '@media': { @@ -1060,11 +1058,11 @@ export const stepsGrid = style({ export const stepCard = style({ minHeight: '170px', - borderRadius: '22px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '22px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', }) export const stepHeader = style({ @@ -1078,10 +1076,11 @@ export const stepMarker = style({ minWidth: '48px', height: '48px', padding: '0 16px', - borderRadius: '18px', + borderRadius: '10px', alignItems: 'center', justifyContent: 'center', - background: '#2563EB', + background: '#296BFF', + border: standardBorderThin, color: '#FFFFFF', fontFamily: 'ptRoot, sans-serif', fontSize: '18px', @@ -1099,7 +1098,7 @@ export const stepBody = style({ marginTop: '14px', fontSize: '15px', lineHeight: 1.65, - color: '#585244', + color: '#666666', }) export const valueGrid = style({ @@ -1155,20 +1154,20 @@ export const useCaseGrid = style({ export const valueCard = style({ minHeight: '150px', - borderRadius: '20px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '20px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', }) export const compactValueCard = style({ minHeight: '136px', - borderRadius: '20px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '18px', - boxShadow: '0 8px 18px rgba(17, 17, 17, 0.04)', + boxShadow: 'none', display: 'flex', flexDirection: 'column', gap: '12px', @@ -1219,9 +1218,9 @@ export const activityList = style({ export const activityCard = style({ display: 'grid', gap: '10px', - borderRadius: '20px', - border: '1px solid #E6E1D4', - background: '#FFFFFF', + borderRadius: '12px', + border: standardBorder, + background: color.background1, padding: '18px', '@media': { 'screen and (min-width: 820px)': { @@ -1239,8 +1238,9 @@ export const activityMeta = style({ padding: '6px 10px', fontSize: '12px', fontWeight: 700, - background: '#F4F1E8', - color: '#575041', + background: color.background2, + color: '#666666', + border: standardBorderThin, }) export const activityTitle = style({ @@ -1253,12 +1253,12 @@ export const activityTitle = style({ export const finalCta = style({ position: 'relative', overflow: 'hidden', - borderRadius: '26px', - border: '1px solid #BFDBFE', - background: 'linear-gradient(135deg, #1D4ED8 0%, #2563EB 58%, #3B82F6 100%)', - color: '#FFFFFF', + borderRadius: '16px', + border: standardBorder, + background: color.background2, + color: '#111111', padding: '30px 22px', - boxShadow: '0 16px 30px rgba(17, 17, 17, 0.14)', + boxShadow: 'none', '@media': { 'screen and (min-width: 768px)': { padding: '40px 34px', @@ -1269,9 +1269,8 @@ export const finalCta = style({ export const finalCtaGlow = style({ position: 'absolute', inset: 0, - background: - 'radial-gradient(circle at 10% 10%, rgba(191, 219, 254, 0.3), transparent 24%), radial-gradient(circle at 92% 20%, rgba(255, 255, 255, 0.16), transparent 22%)', pointerEvents: 'none', + background: 'transparent', }) export const finalCtaContent = style({ @@ -1319,7 +1318,7 @@ export const finalChecklistItem = style({ gap: '10px', fontSize: '15px', lineHeight: 1.55, - color: 'rgba(255, 255, 255, 0.78)', + color: '#666666', '@media': { 'screen and (min-width: 768px)': { alignItems: 'center', @@ -1331,7 +1330,7 @@ export const finalChecklistItem = style({ export const finalChecklistMarker = style({ minWidth: '24px', - color: '#FFFFFF', + color: '#111111', fontWeight: 700, }) @@ -1358,19 +1357,18 @@ export const lightButton = style({ minHeight: '42px', padding: '10px 16px', borderRadius: '999px', - border: '1px solid #2563EB', - background: '#2563EB', + border: standardBorderThin, + background: '#111111', color: '#FFFFFF', fontSize: '14px', fontWeight: 700, textDecoration: 'none', - transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { - borderColor: '#2563EB', - boxShadow: 'inset 0 0 0 1px #2563EB', - backgroundColor: '#EFF6FF', - color: '#1E3A8A', + borderColor: '#111111', + backgroundColor: '#333333', + color: '#FFFFFF', }, '&:focus-visible': focusRing, }, @@ -1379,15 +1377,7 @@ export const lightButton = style({ export const finalPrimaryButton = style([ lightButton, { - border: '1px solid rgba(255, 255, 255, 0.72)', - boxShadow: - '0 0 0 1px rgba(255, 255, 255, 0.24), inset 0 0 0 1px rgba(255, 255, 255, 0.08)', - selectors: { - '&:hover': { - borderColor: '#FFFFFF', - boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.42), inset 0 0 0 1px #2563EB', - }, - }, + borderColor: '#111111', }, ]) @@ -1398,45 +1388,45 @@ export const ghostButton = style({ minHeight: '42px', padding: '10px 16px', borderRadius: '999px', - background: '#FFFFFF', - color: '#1E3A8A', + background: color.background1, + color: '#111111', fontSize: '14px', fontWeight: 700, textDecoration: 'none', - border: '1px solid #DBEAFE', - transition: 'background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease', + border: standardBorderThin, + transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { cursor: 'pointer', - backgroundColor: '#EFF6FF', - borderColor: '#93C5FD', + backgroundColor: color.background2, + borderColor: theme.colors.neutralHover, }, '&:focus-visible': focusRing, }, }) export const subLink = style({ - color: 'rgba(255, 255, 255, 0.86)', + color: '#666666', fontSize: '14px', fontWeight: 700, textDecoration: 'none', width: 'fit-content', selectors: { '&:hover': { - color: '#FFFFFF', + color: '#111111', }, '&:focus-visible': focusRing, }, }) export const heroSubLink = style({ - color: '#2563EB', + color: '#666666', fontSize: '14px', fontWeight: 700, textDecoration: 'none', selectors: { '&:hover': { - color: '#1D4ED8', + color: '#111111', }, '&:focus-visible': focusRing, }, diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index a2964686a..0aaa7f5e5 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -23,8 +23,9 @@ import { SectionIntro } from './SectionIntro' const statusStyles: Record = { Active: { background: '#FFF2BF', color: '#6A5300' }, - Passed: { background: '#DDF7E7', color: '#0F5B37' }, - Funded: { background: '#DCE6FF', color: '#1D3F84' }, + Succeeded: { background: '#DDF7E7', color: '#0F5B37' }, + Queued: { background: '#DCE6FF', color: '#1D3F84' }, + Defeated: { background: '#F2F4F7', color: '#4F5B6C' }, Executed: { background: '#ECE7FF', color: '#44348F' }, Trending: { background: '#FFE1D7', color: '#7D2E0B' }, Live: { background: '#DDF7E7', color: '#0F5B37' }, diff --git a/apps/web/src/modules/about/data.ts b/apps/web/src/modules/about/data.ts index 8fc39b32e..30dd8a236 100644 --- a/apps/web/src/modules/about/data.ts +++ b/apps/web/src/modules/about/data.ts @@ -117,7 +117,7 @@ export const proposalHighlights: DroposalHighlight[] = [ title: 'Operator tooling sprint for treasury reporting', dao: 'BuilderDAO', amount: '42 voters', - status: 'Funded', + status: 'Queued', summary: 'Shared treasury analytics and operator tooling improve governance workflows across Builder DAOs.', href: '/explore?search=builderdao', @@ -128,7 +128,7 @@ export const proposalHighlights: DroposalHighlight[] = [ title: 'Mini grants for local Nouns Fest activations', dao: 'Nouns Fest', amount: '18 voters', - status: 'Passed', + status: 'Succeeded', summary: 'Regional teams get budget for activations, documentation, and post-event publishing.', href: '/explore?search=nouns%20fest', @@ -150,7 +150,7 @@ export const proposalHighlights: DroposalHighlight[] = [ title: 'Residency round for new onchain builders', dao: 'Far House', amount: '30 voters', - status: 'Trending', + status: 'Defeated', summary: 'Funds a cohort of builders experimenting with coining, drops, and governance flows.', href: '/explore?search=far%20house', diff --git a/apps/web/src/modules/about/types.ts b/apps/web/src/modules/about/types.ts index c03a73a5b..4dee0cde5 100644 --- a/apps/web/src/modules/about/types.ts +++ b/apps/web/src/modules/about/types.ts @@ -43,8 +43,9 @@ export type CoiningHighlight = { export type DroposalStatus = | 'Active' - | 'Passed' - | 'Funded' + | 'Succeeded' + | 'Queued' + | 'Defeated' | 'Executed' | 'Trending' | 'Live' diff --git a/apps/web/src/pages/api/about/showcase.ts b/apps/web/src/pages/api/about/showcase.ts index 9f9a4b952..06533c674 100644 --- a/apps/web/src/pages/api/about/showcase.ts +++ b/apps/web/src/pages/api/about/showcase.ts @@ -60,6 +60,9 @@ type ShowcaseProposal = { timeCreated: string voteEnd: string voteCount: number + forVotes: number + againstVotes: number + quorumVotes: string queued: boolean executed: boolean canceled: boolean @@ -132,6 +135,9 @@ const ABOUT_SHOWCASE_QUERY = gql` timeCreated voteEnd voteCount + forVotes + againstVotes + quorumVotes queued executed canceled @@ -191,9 +197,14 @@ const deriveProposalStatus = ( ): DroposalStatus | null => { if (proposal.canceled || proposal.vetoed) return null if (proposal.executed) return 'Executed' - if (proposal.queued) return 'Funded' + if (proposal.queued) return 'Queued' if (Number(proposal.voteEnd) > now) return 'Active' - return 'Passed' + + const passed = + proposal.forVotes > proposal.againstVotes && + BigInt(proposal.forVotes) >= BigInt(proposal.quorumVotes) + + return passed ? 'Succeeded' : 'Defeated' } const fetchShowcaseDataForChain = async ( From a65eba2ffa85fb0eee911c6e6856c08dfbb21a41 Mon Sep 17 00:00:00 2001 From: xSatori <99294685+xSatori@users.noreply.github.com> Date: Wed, 6 May 2026 19:38:35 -0400 Subject: [PATCH 04/13] Minor mobile and desktop UI tweaks --- apps/web/src/modules/about/AboutPage.css.ts | 52 +++++++++++++++++-- apps/web/src/modules/about/AboutPage.tsx | 5 +- .../components/DroposalHighlightsSection.tsx | 10 ++-- .../about/components/FeaturedDaoSection.tsx | 8 ++- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index 96bd179ca..2de72e0fb 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -622,6 +622,13 @@ export const tabs = style({ display: 'flex', flexWrap: 'wrap', gap: '10px', + '@media': { + 'screen and (max-width: 520px)': { + width: '100%', + flexWrap: 'nowrap', + gap: '6px', + }, + }, }) export const tabButton = style({ @@ -642,6 +649,15 @@ export const tabButton = style({ }, '&:focus-visible': focusRing, }, + '@media': { + 'screen and (max-width: 520px)': { + flex: '1 1 0', + minWidth: 0, + minHeight: '38px', + padding: '8px 6px', + fontSize: '13px', + }, + }, }) export const activeTabButton = style({ @@ -671,6 +687,32 @@ export const daoGrid = style({ }, }) +export const mobileStatsHeading = style({ + display: 'block', + margin: '28px 0 14px', + fontFamily: 'ptRoot, sans-serif', + fontSize: '24px', + lineHeight: 1.1, + color: '#111111', + '@media': { + 'screen and (min-width: 768px)': { + display: 'none', + }, + }, +}) + +export const statsBlock = style({ + marginTop: '28px', + '@media': { + 'screen and (max-width: 767px)': { + marginTop: 0, + }, + 'screen and (min-width: 768px)': { + marginTop: '48px', + }, + }, +}) + export const daoCard = style({ display: 'flex', flexDirection: 'column', @@ -1162,20 +1204,20 @@ export const valueCard = style({ }) export const compactValueCard = style({ - minHeight: '136px', + minHeight: '96px', borderRadius: '12px', border: standardBorder, background: color.background1, - padding: '18px', + padding: '16px', boxShadow: 'none', display: 'flex', flexDirection: 'column', - gap: '12px', + gap: '10px', minWidth: 0, '@media': { 'screen and (min-width: 768px)': { - minHeight: '160px', - padding: '20px', + minHeight: '108px', + padding: '18px', }, }, }) diff --git a/apps/web/src/modules/about/AboutPage.tsx b/apps/web/src/modules/about/AboutPage.tsx index d7484ce80..27abe0452 100644 --- a/apps/web/src/modules/about/AboutPage.tsx +++ b/apps/web/src/modules/about/AboutPage.tsx @@ -80,7 +80,10 @@ export const AboutPageView: React.FC = () => { - + diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index 0aaa7f5e5..17ff90b11 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -38,6 +38,7 @@ type DroposalHighlightsSectionProps = { title?: string copy?: string linkLabel?: string + showStatusBadge?: boolean } export const DroposalHighlightsSection: React.FC = ({ @@ -46,6 +47,7 @@ export const DroposalHighlightsSection: React.FC title = 'Drops turn releases into onchain distribution', copy = 'Launch collectible drops that turn media, editions, and releases into distribution, ownership, and treasury growth for decentralized communities.', linkLabel = 'View drop', + showStatusBadge = true, }) => { const highlights = items?.length ? items : dropHighlights @@ -64,9 +66,11 @@ export const DroposalHighlightsSection: React.FC > {proposal.dao} - - {proposal.status} - + {showStatusBadge ? ( + + {proposal.status} + + ) : null} {getChainLogoSrc(proposal.category) ? ( = ({ ))} - + + Protocol Stats + + + From eb5f3be51872d32351bbd4caa5a9551fd9b0fae7 Mon Sep 17 00:00:00 2001 From: xSatori <99294685+xSatori@users.noreply.github.com> Date: Thu, 7 May 2026 16:15:57 -0400 Subject: [PATCH 05/13] UI styling tweaks --- apps/web/src/modules/about/AboutPage.css.ts | 120 ++++++++++++++---- .../modules/about/components/CoiningCard.tsx | 12 +- .../src/modules/about/components/DaoCard.tsx | 14 +- .../components/DroposalHighlightsSection.tsx | 13 +- .../about/components/WhyBuilderSection.tsx | 109 +++++++++++++--- 5 files changed, 214 insertions(+), 54 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index 2de72e0fb..a9f38181e 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -1,5 +1,5 @@ import { color, theme } from '@buildeross/zord' -import { keyframes, style } from '@vanilla-extract/css' +import { globalStyle, keyframes, style } from '@vanilla-extract/css' const focusRing = { outline: `2px solid ${theme.colors.border}`, @@ -8,6 +8,8 @@ const focusRing = { const standardBorder = '2px solid #111111' const standardBorderThin = '1px solid #111111' +const softBlueBackground = '#E8F4FF' +const softBlueBorder = '#296BFF' const marqueeScroll = keyframes({ '0%': { @@ -266,8 +268,8 @@ export const secondaryButton = style({ selectors: { '&:hover': { cursor: 'pointer', - backgroundColor: color.background2, - borderColor: theme.colors.neutralHover, + backgroundColor: softBlueBackground, + borderColor: softBlueBorder, }, '&:focus-visible': focusRing, }, @@ -358,7 +360,7 @@ export const logoMarquee = style({ minHeight: '96px', overflow: 'hidden', borderRadius: '10px', - background: color.background2, + background: color.background1, '@media': { 'screen and (min-width: 768px)': { minHeight: '160px', @@ -377,7 +379,7 @@ export const logoMarqueeTrack = style({ position: 'absolute', inset: 0, background: - 'linear-gradient(90deg, rgba(242, 242, 242, 1) 0%, rgba(242, 242, 242, 0) 10%, rgba(242, 242, 242, 0) 90%, rgba(242, 242, 242, 1) 100%)', + 'linear-gradient(90deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 10%, rgba(255, 255, 255, 0) 90%, rgba(255, 255, 255, 1) 100%)', zIndex: 2, pointerEvents: 'none', }, @@ -407,7 +409,6 @@ export const logoMarqueeItem = style({ borderRadius: '12px', overflow: 'hidden', background: color.background1, - border: standardBorderThin, boxShadow: 'none', display: 'flex', alignItems: 'center', @@ -500,6 +501,7 @@ export const heroFooterValue = style({ export const statGrid = style({ display: 'grid', + width: '100%', gap: '16px', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', '@media': { @@ -524,8 +526,8 @@ export const statCard = style({ transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - borderColor: theme.colors.neutralHover, - backgroundColor: color.background2, + borderColor: softBlueBorder, + backgroundColor: softBlueBackground, }, }, }) @@ -644,8 +646,8 @@ export const tabButton = style({ selectors: { '&:hover': { cursor: 'pointer', - borderColor: theme.colors.neutralHover, - backgroundColor: color.background2, + borderColor: softBlueBorder, + backgroundColor: softBlueBackground, }, '&:focus-visible': focusRing, }, @@ -661,13 +663,13 @@ export const tabButton = style({ }) export const activeTabButton = style({ - background: color.background2, - borderColor: theme.colors.neutralHover, + background: softBlueBackground, + borderColor: softBlueBorder, color: '#111111', selectors: { '&:hover': { - borderColor: theme.colors.neutralHover, - backgroundColor: color.background2, + borderColor: softBlueBorder, + backgroundColor: softBlueBackground, color: '#111111', }, }, @@ -675,6 +677,7 @@ export const activeTabButton = style({ export const daoGrid = style({ display: 'grid', + width: '100%', gap: '16px', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', '@media': { @@ -724,12 +727,16 @@ export const daoCard = style({ padding: '18px', boxShadow: 'none', minWidth: 0, + color: 'inherit', + textDecoration: 'none', transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - borderColor: theme.colors.neutralHover, - backgroundColor: color.background2, + borderColor: softBlueBorder, + backgroundColor: softBlueBackground, + cursor: 'pointer', }, + '&:focus-visible': focusRing, }, '@media': { 'screen and (min-width: 768px)': { @@ -808,6 +815,15 @@ export const daoDescription = style({ color: '#666666', }) +globalStyle(`${daoDescription} p`, { + margin: 0, +}) + +globalStyle(`${daoDescription} strong`, { + fontWeight: 700, + color: '#555555', +}) + export const badge = style({ display: 'inline-flex', width: 'fit-content', @@ -900,12 +916,16 @@ export const coiningCard = style({ padding: '18px', boxShadow: 'none', minWidth: 0, + color: 'inherit', + textDecoration: 'none', transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - borderColor: theme.colors.neutralHover, - backgroundColor: color.background2, + borderColor: softBlueBorder, + backgroundColor: softBlueBackground, + cursor: 'pointer', }, + '&:focus-visible': focusRing, }, '@media': { 'screen and (min-width: 768px)': { @@ -1022,12 +1042,16 @@ export const droposalCard = style({ background: color.background1, padding: '18px', boxShadow: 'none', + color: 'inherit', + textDecoration: 'none', transition: 'border-color 0.15s ease, background-color 0.15s ease', selectors: { '&:hover': { - borderColor: theme.colors.neutralHover, - backgroundColor: color.background2, + borderColor: softBlueBorder, + backgroundColor: softBlueBackground, + cursor: 'pointer', }, + '&:focus-visible': focusRing, }, '@media': { 'screen and (min-width: 880px)': { @@ -1122,7 +1146,7 @@ export const stepMarker = style({ alignItems: 'center', justifyContent: 'center', background: '#296BFF', - border: standardBorderThin, + border: 'none', color: '#FFFFFF', fontFamily: 'ptRoot, sans-serif', fontSize: '18px', @@ -1157,12 +1181,13 @@ export const valueGrid = style({ }, }) -export const compactValueGrid = style({ +export const featureStrip = style({ + position: 'relative', display: 'grid', width: '100%', maxWidth: '100%', alignSelf: 'stretch', - gap: '16px', + gap: '18px', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', '@media': { 'screen and (min-width: 700px)': { @@ -1170,10 +1195,55 @@ export const compactValueGrid = style({ }, 'screen and (min-width: 1080px)': { gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', + gap: '24px', + }, + }, +}) + +export const featureItem = style({ + display: 'grid', + gridTemplateColumns: '42px minmax(0, 1fr)', + gap: '12px', + alignItems: 'center', + minWidth: 0, + '@media': { + 'screen and (min-width: 700px)': { + gridTemplateColumns: '42px minmax(0, 1fr)', + justifyItems: 'stretch', + alignItems: 'center', + gap: '12px', }, }, }) +export const featureIconBadge = style({ + position: 'relative', + zIndex: 1, + width: '42px', + height: '42px', + borderRadius: '14px', + background: color.background2, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + color: '#111111', +}) + +globalStyle(`${featureIconBadge} svg`, { + width: '20px', + height: '20px', + display: 'block', +}) + +export const featureLabel = style({ + fontFamily: 'ptRoot, sans-serif', + fontSize: '17px', + lineHeight: 1.2, + fontWeight: 700, + color: '#111111', + overflowWrap: 'anywhere', +}) + export const useCaseGrid = style({ display: 'grid', width: '100%', @@ -1440,8 +1510,8 @@ export const ghostButton = style({ selectors: { '&:hover': { cursor: 'pointer', - backgroundColor: color.background2, - borderColor: theme.colors.neutralHover, + backgroundColor: softBlueBackground, + borderColor: softBlueBorder, }, '&:focus-visible': focusRing, }, diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx index 1b10355e5..d3c26794b 100644 --- a/apps/web/src/modules/about/components/CoiningCard.tsx +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -26,7 +26,11 @@ export const CoiningCard: React.FC = ({ item }) => { const chainLogoSrc = getChainLogoSrc(item.chainLabel) return ( - + {item.amount} @@ -51,9 +55,9 @@ export const CoiningCard: React.FC = ({ item }) => { - + View post - - + + ) } diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx index aa5fa7782..7fec68c5e 100644 --- a/apps/web/src/modules/about/components/DaoCard.tsx +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -1,4 +1,4 @@ -import { FallbackImage } from '@buildeross/ui' +import { FallbackImage, MarkdownDisplay } from '@buildeross/ui' import { Box, Flex, Text } from '@buildeross/zord' import Link from 'next/link' import React from 'react' @@ -26,7 +26,7 @@ type DaoCardProps = { export const DaoCard: React.FC = ({ dao }) => { return ( - + = ({ dao }) => { {dao.name} - {dao.description} + + {dao.description} + {dao.chainIcon ? ( @@ -65,9 +67,9 @@ export const DaoCard: React.FC = ({ dao }) => { {dao.signalValue} - + View DAO - - + + ) } diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index 17ff90b11..119a43b33 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -57,7 +57,12 @@ export const DroposalHighlightsSection: React.FC {highlights.map((proposal) => ( - + {proposal.amount} - + {linkLabel} - + - + ))} diff --git a/apps/web/src/modules/about/components/WhyBuilderSection.tsx b/apps/web/src/modules/about/components/WhyBuilderSection.tsx index 6fdf7cb38..1be68577a 100644 --- a/apps/web/src/modules/about/components/WhyBuilderSection.tsx +++ b/apps/web/src/modules/about/components/WhyBuilderSection.tsx @@ -2,15 +2,103 @@ import { Box, Text } from '@buildeross/zord' import React from 'react' import { - compactValueCard, - compactValueGrid, - compactValueTitle, - mutedText, + featureIconBadge, + featureItem, + featureLabel, + featureStrip, section, } from '../AboutPage.css' import { builderValueProps } from '../data' +import type { BuilderValueProp } from '../types' import { SectionIntro } from './SectionIntro' +type FeatureIcon = 'rocket' | 'treasury' | 'governance' | 'creative' + +const featureIconsById: Record = { + 'launch-fast': 'rocket', + 'treasury-auctions': 'treasury', + 'governance-day-one': 'governance', + 'creative-output': 'creative', +} + +const FeatureIconSvg = ({ icon }: { icon: FeatureIcon }) => { + switch (icon) { + case 'rocket': + return ( + + ) + case 'treasury': + return ( + + ) + case 'governance': + return ( + + ) + case 'creative': + return ( + + ) + } +} + +const FeatureItem = ({ item }: { item: BuilderValueProp }) => ( + + + + + + {item.title} + + +) + export const WhyBuilderSection: React.FC = () => { return ( @@ -20,18 +108,9 @@ export const WhyBuilderSection: React.FC = () => { copy="Most DAO stacks require multiple tools to manage governance, funding, and coordination. Builder combines all of it into a single onchain system. You don't assemble a stack. You launch a DAO, with no coding required." /> - + {builderValueProps.map((item) => ( - - - {item.title} - - {item.body ? ( - - {item.body} - - ) : null} - + ))} From 1421aaf3172284812801920c78b79b348ad0b056 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Fri, 8 May 2026 12:41:36 +0530 Subject: [PATCH 06/13] fix: align about page with dark mode theme tokens Replace hardcoded and data-driven colors on the About page with theme variables so dark/light mode backgrounds, focus states, and hover feedback render consistently. --- apps/web/src/modules/about/AboutPage.css.ts | 317 ++++++++++++------ apps/web/src/modules/about/AboutPage.tsx | 16 +- .../modules/about/components/CoiningCard.tsx | 10 +- .../src/modules/about/components/DaoCard.tsx | 14 +- .../components/DroposalHighlightsSection.tsx | 36 +- .../about/components/EcosystemStatGrid.tsx | 27 +- apps/web/src/modules/about/data.ts | 8 - apps/web/src/modules/about/types.ts | 5 - apps/web/src/pages/api/about/dao-tabs.ts | 11 +- apps/web/src/pages/api/about/showcase.ts | 11 +- apps/web/src/pages/api/about/snapshot.ts | 6 - 11 files changed, 293 insertions(+), 168 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index a9f38181e..05cb7ff68 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -1,15 +1,15 @@ -import { color, theme } from '@buildeross/zord' +import { theme } from '@buildeross/zord' import { globalStyle, keyframes, style } from '@vanilla-extract/css' const focusRing = { - outline: `2px solid ${theme.colors.border}`, + outline: `2px solid ${theme.colors.focusRing}`, outlineOffset: '2px', } -const standardBorder = '2px solid #111111' -const standardBorderThin = '1px solid #111111' -const softBlueBackground = '#E8F4FF' -const softBlueBorder = '#296BFF' +const standardBorder = `2px solid ${theme.colors.border}` +const standardBorderThin = `1px solid ${theme.colors.border}` +const softBlueBackground = `color-mix(in srgb, ${theme.colors.focusRing} 14%, ${theme.colors.background1})` +const softBlueBorder = theme.colors.focusRing const marqueeScroll = keyframes({ '0%': { @@ -24,7 +24,7 @@ export const page = style({ width: '100%', padding: '48px 16px 96px', boxSizing: 'border-box', - background: color.background1, + background: theme.colors.background1, overflowX: 'clip', '@media': { 'screen and (min-width: 768px)': { @@ -91,13 +91,13 @@ export const eyebrow = style({ width: 'fit-content', borderRadius: '999px', padding: '4px 10px', - background: color.background2, + background: theme.colors.background2, border: standardBorderThin, fontSize: '12px', lineHeight: '16px', letterSpacing: '0.04em', textTransform: 'uppercase', - color: '#666666', + color: theme.colors.text3, fontWeight: 700, }) @@ -107,7 +107,7 @@ export const sectionTitle = style({ lineHeight: 1.04, whiteSpace: 'normal', overflowWrap: 'anywhere', - color: '#111111', + color: theme.colors.text1, '@media': { 'screen and (min-width: 768px)': { fontSize: '40px', @@ -124,13 +124,13 @@ export const sectionTitleOnly = style({ export const sectionCopy = style({ fontSize: '16px', lineHeight: 1.55, - color: '#666666', + color: theme.colors.text3, }) export const introCopyNoWrap = style({ fontSize: '16px', lineHeight: 1.55, - color: '#666666', + color: theme.colors.text3, whiteSpace: 'normal', '@media': { 'screen and (min-width: 960px)': { @@ -164,7 +164,7 @@ export const heroTitle = style({ fontSize: '36px', lineHeight: 1, letterSpacing: '-0.03em', - color: '#111111', + color: theme.colors.text1, maxWidth: '760px', overflowWrap: 'anywhere', '@media': { @@ -178,7 +178,7 @@ export const heroText = style({ maxWidth: '640px', fontSize: '16px', lineHeight: 1.6, - color: '#666666', + color: theme.colors.text3, '@media': { 'screen and (min-width: 768px)': { fontSize: '18px', @@ -202,14 +202,14 @@ export const heroHighlight = style({ gap: '10px', fontSize: '14px', lineHeight: 1.5, - color: '#444444', + color: theme.colors.text2, }) export const heroHighlightDot = style({ width: '8px', height: '8px', borderRadius: '999px', - background: '#111111', + background: theme.colors.text1, flexShrink: 0, marginTop: '6px', }) @@ -222,17 +222,17 @@ export const primaryButton = style({ padding: '10px 16px', borderRadius: '999px', border: standardBorderThin, - color: '#FFFFFF', - background: '#111111', + color: theme.colors.background1, + background: theme.colors.text1, fontSize: '14px', fontWeight: 700, textDecoration: 'none', transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { - borderColor: '#111111', - backgroundColor: '#333333', - color: '#FFFFFF', + borderColor: theme.colors.border, + backgroundColor: theme.colors.text2, + color: theme.colors.background1, }, '&:focus-visible': focusRing, }, @@ -259,8 +259,8 @@ export const secondaryButton = style({ padding: '10px 16px', borderRadius: '999px', border: standardBorderThin, - color: '#111111', - background: color.background1, + color: theme.colors.text1, + background: theme.colors.background1, fontSize: '14px', fontWeight: 700, textDecoration: 'none', @@ -280,7 +280,7 @@ export const heroPanel = style({ overflow: 'hidden', borderRadius: '16px', border: standardBorder, - background: color.background2, + background: theme.colors.background2, padding: '14px', minHeight: 'auto', boxShadow: 'none', @@ -317,7 +317,7 @@ export const montageGrid = style({ export const montageCard = style({ borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '16px', boxShadow: 'none', minWidth: 0, @@ -360,7 +360,7 @@ export const logoMarquee = style({ minHeight: '96px', overflow: 'hidden', borderRadius: '10px', - background: color.background1, + background: theme.colors.background1, '@media': { 'screen and (min-width: 768px)': { minHeight: '160px', @@ -408,7 +408,7 @@ export const logoMarqueeItem = style({ height: '64px', borderRadius: '12px', overflow: 'hidden', - background: color.background1, + background: theme.colors.background1, boxShadow: 'none', display: 'flex', alignItems: 'center', @@ -434,7 +434,7 @@ export const montageLabel = style({ fontSize: '12px', letterSpacing: '0.08em', textTransform: 'uppercase', - color: '#808080', + color: theme.colors.text4, fontWeight: 700, }) @@ -443,7 +443,7 @@ export const montageValue = style({ fontSize: '24px', lineHeight: 1, whiteSpace: 'pre-line', - color: '#111111', + color: theme.colors.text1, '@media': { 'screen and (min-width: 768px)': { marginTop: '10px', @@ -456,7 +456,7 @@ export const montageBody = style({ marginTop: '10px', fontSize: '14px', lineHeight: 1.55, - color: '#666666', + color: theme.colors.text3, }) export const daoMiniList = style({ @@ -472,7 +472,7 @@ export const daoMiniCard = style({ gap: '12px', padding: '12px 14px', borderRadius: '16px', - background: '#EFF6FF', + background: theme.colors.background2, }) export const daoMiniAvatar = style({ @@ -484,7 +484,7 @@ export const daoMiniAvatar = style({ justifyContent: 'center', fontWeight: 700, fontSize: '14px', - color: '#111111', + color: theme.colors.text1, }) export const heroFooterStat = style({ @@ -496,7 +496,7 @@ export const heroFooterStat = style({ export const heroFooterValue = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '24px', - color: '#111111', + color: theme.colors.text1, }) export const statGrid = style({ @@ -520,7 +520,7 @@ export const statCard = style({ minHeight: '178px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '22px', boxShadow: 'none', transition: 'border-color 0.15s ease, background-color 0.15s ease', @@ -547,6 +547,13 @@ export const statAccent = style({ lineHeight: 1, }) +export const statAccentDao = style({ background: theme.colors.background2 }) +export const statAccentTreasury = style({ background: theme.colors.positive }) +export const statAccentAuction = style({ background: theme.colors.warning }) +export const statAccentProposal = style({ background: theme.colors.accent }) +export const statAccentMembers = style({ background: theme.colors.negative }) +export const statAccentTokens = style({ background: theme.colors.accent }) + export const statLabel = style({ position: 'relative', zIndex: 1, @@ -554,7 +561,7 @@ export const statLabel = style({ fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', - color: '#808080', + color: theme.colors.text4, }) export const statValue = style({ @@ -564,7 +571,7 @@ export const statValue = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '36px', lineHeight: 1, - color: '#111111', + color: theme.colors.text1, '@media': { 'screen and (min-width: 768px)': { fontSize: '42px', @@ -579,7 +586,7 @@ export const statDetail = style({ maxWidth: '28ch', fontSize: '14px', lineHeight: 1.55, - color: '#5A5347', + color: theme.colors.text3, }) export const sectionTopRow = style({ @@ -616,7 +623,7 @@ export const sectionInlineCopy = style({ maxWidth: '760px', fontSize: '16px', lineHeight: 1.55, - color: '#666666', + color: theme.colors.text3, margin: 0, }) @@ -637,11 +644,11 @@ export const tabButton = style({ minHeight: '42px', borderRadius: '999px', border: standardBorderThin, - background: color.background1, + background: theme.colors.background1, padding: '10px 16px', fontSize: '14px', fontWeight: 700, - color: '#111111', + color: theme.colors.text1, transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { @@ -665,12 +672,12 @@ export const tabButton = style({ export const activeTabButton = style({ background: softBlueBackground, borderColor: softBlueBorder, - color: '#111111', + color: theme.colors.text1, selectors: { '&:hover': { borderColor: softBlueBorder, backgroundColor: softBlueBackground, - color: '#111111', + color: theme.colors.text1, }, }, }) @@ -696,7 +703,7 @@ export const mobileStatsHeading = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '24px', lineHeight: 1.1, - color: '#111111', + color: theme.colors.text1, '@media': { 'screen and (min-width: 768px)': { display: 'none', @@ -723,7 +730,7 @@ export const daoCard = style({ minHeight: '320px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '18px', boxShadow: 'none', minWidth: 0, @@ -757,7 +764,7 @@ export const daoChainBadge = style({ height: '34px', borderRadius: '10px', border: standardBorderThin, - background: color.background1, + background: theme.colors.background1, boxShadow: 'none', display: 'inline-flex', alignItems: 'center', @@ -794,6 +801,23 @@ export const daoAvatar = style({ overflow: 'hidden', }) +export const daoAvatarSurfaceA = style({ + background: theme.colors.background2, + color: theme.colors.text1, +}) +export const daoAvatarSurfaceB = style({ + background: theme.colors.accent, + color: theme.colors.text1, +}) +export const daoAvatarSurfaceC = style({ + background: theme.colors.positive, + color: theme.colors.text1, +}) +export const daoAvatarSurfaceD = style({ + background: theme.colors.warning, + color: theme.colors.text1, +}) + export const daoAvatarImage = style({ width: '100%', height: '100%', @@ -805,14 +829,14 @@ export const daoName = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '20px', lineHeight: 1.05, - color: '#111111', + color: theme.colors.text1, overflowWrap: 'anywhere', }) export const daoDescription = style({ fontSize: '14px', lineHeight: 1.6, - color: '#666666', + color: theme.colors.text3, }) globalStyle(`${daoDescription} p`, { @@ -821,7 +845,7 @@ globalStyle(`${daoDescription} p`, { globalStyle(`${daoDescription} strong`, { fontWeight: 700, - color: '#555555', + color: theme.colors.text2, }) export const badge = style({ @@ -840,14 +864,14 @@ export const daoSignal = style({ marginTop: 'auto', padding: '14px 16px', borderRadius: '10px', - background: color.background2, + background: theme.colors.background2, }) export const daoSignalLabel = style({ fontSize: '12px', textTransform: 'uppercase', letterSpacing: '0.06em', - color: '#808080', + color: theme.colors.text4, fontWeight: 700, }) @@ -857,7 +881,7 @@ export const daoSignalValue = style({ lineHeight: 1.2, whiteSpace: 'normal', overflowWrap: 'anywhere', - color: '#111111', + color: theme.colors.text1, }) export const cardLink = style({ @@ -865,7 +889,7 @@ export const cardLink = style({ alignItems: 'center', justifyContent: 'space-between', gap: '10px', - color: '#111111', + color: theme.colors.text1, fontSize: '14px', fontWeight: 700, textDecoration: 'none', @@ -890,7 +914,7 @@ export const scrollRow = style({ height: '10px', }, '&::-webkit-scrollbar-thumb': { - background: '#D6D1C4', + background: theme.colors.border, borderRadius: '999px', }, }, @@ -912,7 +936,7 @@ export const coiningCard = style({ scrollSnapAlign: 'start', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '18px', boxShadow: 'none', minWidth: 0, @@ -951,6 +975,11 @@ export const coiningPreview = style({ }, }) +export const coiningPreviewSurfaceA = style({ background: theme.colors.accent }) +export const coiningPreviewSurfaceB = style({ background: theme.colors.warning }) +export const coiningPreviewSurfaceC = style({ background: theme.colors.positive }) +export const coiningPreviewSurfaceD = style({ background: theme.colors.background2 }) + export const coiningPreviewTop = style({ display: 'flex', alignItems: 'flex-start', @@ -962,12 +991,12 @@ export const coiningPreviewMark = style({ alignSelf: 'flex-start', display: 'inline-flex', borderRadius: '999px', - background: color.background1, + background: theme.colors.background1, border: standardBorderThin, padding: '6px 10px', fontSize: '12px', fontWeight: 700, - color: '#111111', + color: theme.colors.text1, }) export const coiningNetworkBadge = style({ @@ -975,7 +1004,7 @@ export const coiningNetworkBadge = style({ height: '34px', borderRadius: '10px', border: standardBorderThin, - background: color.background1, + background: theme.colors.background1, boxShadow: 'none', display: 'inline-flex', alignItems: 'center', @@ -989,7 +1018,7 @@ export const coiningPreviewTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '26px', lineHeight: 0.95, - color: '#111111', + color: theme.colors.text1, overflowWrap: 'anywhere', '@media': { 'screen and (min-width: 768px)': { @@ -1008,13 +1037,13 @@ export const coiningTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '22px', lineHeight: 1.05, - color: '#111111', + color: theme.colors.text1, }) export const mutedText = style({ fontSize: '14px', lineHeight: 1.6, - color: '#666666', + color: theme.colors.text3, }) export const amountPill = style({ @@ -1022,8 +1051,8 @@ export const amountPill = style({ width: 'fit-content', borderRadius: '999px', padding: '7px 12px', - background: color.background2, - color: '#111111', + background: theme.colors.background2, + color: theme.colors.text1, border: standardBorderThin, fontSize: '13px', fontWeight: 700, @@ -1039,7 +1068,7 @@ export const droposalCard = style({ gap: '14px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '18px', boxShadow: 'none', color: 'inherit', @@ -1079,11 +1108,40 @@ export const statusBadge = style({ fontWeight: 700, }) +export const statusLive = style({ background: theme.colors.positive, color: theme.colors.text1 }) +export const statusRecent = style({ background: theme.colors.accent, color: theme.colors.text1 }) +export const statusActive = style({ background: theme.colors.warning, color: theme.colors.text1 }) +export const statusSucceeded = style({ + background: theme.colors.positive, + color: theme.colors.text1, +}) +export const statusQueued = style({ + background: theme.colors.background2, + color: theme.colors.text1, +}) +export const statusDefeated = style({ + background: theme.colors.background2, + color: theme.colors.text2, +}) +export const statusExecuted = style({ + background: theme.colors.background2, + color: theme.colors.text1, +}) +export const statusTrending = style({ + background: theme.colors.negative, + color: theme.colors.text1, +}) + +export const daoBadge = style({ + background: theme.colors.background2, + color: theme.colors.text2, +}) + export const droposalTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '20px', lineHeight: 1.05, - color: '#111111', + color: theme.colors.text1, overflowWrap: 'anywhere', '@media': { 'screen and (min-width: 768px)': { @@ -1095,7 +1153,7 @@ export const droposalTitle = style({ export const droposalSummary = style({ fontSize: '15px', lineHeight: 1.65, - color: '#5A5348', + color: theme.colors.text3, maxWidth: '72ch', }) @@ -1126,7 +1184,7 @@ export const stepCard = style({ minHeight: '170px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '22px', boxShadow: 'none', }) @@ -1145,9 +1203,9 @@ export const stepMarker = style({ borderRadius: '10px', alignItems: 'center', justifyContent: 'center', - background: '#296BFF', + background: theme.colors.accent, border: 'none', - color: '#FFFFFF', + color: theme.colors.background1, fontFamily: 'ptRoot, sans-serif', fontSize: '18px', flexShrink: 0, @@ -1157,14 +1215,14 @@ export const stepTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '28px', lineHeight: 1.02, - color: '#111111', + color: theme.colors.text1, }) export const stepBody = style({ marginTop: '14px', fontSize: '15px', lineHeight: 1.65, - color: '#666666', + color: theme.colors.text3, }) export const valueGrid = style({ @@ -1222,11 +1280,11 @@ export const featureIconBadge = style({ width: '42px', height: '42px', borderRadius: '14px', - background: color.background2, + background: theme.colors.background2, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - color: '#111111', + color: theme.colors.text1, }) globalStyle(`${featureIconBadge} svg`, { @@ -1240,7 +1298,7 @@ export const featureLabel = style({ fontSize: '17px', lineHeight: 1.2, fontWeight: 700, - color: '#111111', + color: theme.colors.text1, overflowWrap: 'anywhere', }) @@ -1268,7 +1326,7 @@ export const valueCard = style({ minHeight: '150px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '20px', boxShadow: 'none', }) @@ -1277,7 +1335,7 @@ export const compactValueCard = style({ minHeight: '96px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '16px', boxShadow: 'none', display: 'flex', @@ -1296,14 +1354,14 @@ export const valueTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '24px', lineHeight: 1.05, - color: '#111111', + color: theme.colors.text1, }) export const compactValueTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '18px', lineHeight: 1.2, - color: '#111111', + color: theme.colors.text1, display: '-webkit-box', overflow: 'hidden', WebkitLineClamp: 3, @@ -1332,7 +1390,7 @@ export const activityCard = style({ gap: '10px', borderRadius: '12px', border: standardBorder, - background: color.background1, + background: theme.colors.background1, padding: '18px', '@media': { 'screen and (min-width: 820px)': { @@ -1350,8 +1408,8 @@ export const activityMeta = style({ padding: '6px 10px', fontSize: '12px', fontWeight: 700, - background: color.background2, - color: '#666666', + background: theme.colors.background2, + color: theme.colors.text3, border: standardBorderThin, }) @@ -1359,7 +1417,7 @@ export const activityTitle = style({ fontFamily: 'ptRoot, sans-serif', fontSize: '22px', lineHeight: 1.08, - color: '#111111', + color: theme.colors.text1, }) export const finalCta = style({ @@ -1367,8 +1425,8 @@ export const finalCta = style({ overflow: 'hidden', borderRadius: '16px', border: standardBorder, - background: color.background2, - color: '#111111', + background: theme.colors.background2, + color: theme.colors.text1, padding: '30px 22px', boxShadow: 'none', '@media': { @@ -1430,7 +1488,7 @@ export const finalChecklistItem = style({ gap: '10px', fontSize: '15px', lineHeight: 1.55, - color: '#666666', + color: theme.colors.text3, '@media': { 'screen and (min-width: 768px)': { alignItems: 'center', @@ -1442,7 +1500,7 @@ export const finalChecklistItem = style({ export const finalChecklistMarker = style({ minWidth: '24px', - color: '#111111', + color: theme.colors.text1, fontWeight: 700, }) @@ -1470,17 +1528,17 @@ export const lightButton = style({ padding: '10px 16px', borderRadius: '999px', border: standardBorderThin, - background: '#111111', - color: '#FFFFFF', + background: theme.colors.text1, + color: theme.colors.background1, fontSize: '14px', fontWeight: 700, textDecoration: 'none', transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', selectors: { '&:hover': { - borderColor: '#111111', - backgroundColor: '#333333', - color: '#FFFFFF', + borderColor: theme.colors.border, + backgroundColor: theme.colors.text2, + color: theme.colors.background1, }, '&:focus-visible': focusRing, }, @@ -1489,7 +1547,7 @@ export const lightButton = style({ export const finalPrimaryButton = style([ lightButton, { - borderColor: '#111111', + borderColor: theme.colors.border, }, ]) @@ -1500,8 +1558,8 @@ export const ghostButton = style({ minHeight: '42px', padding: '10px 16px', borderRadius: '999px', - background: color.background1, - color: '#111111', + background: theme.colors.background1, + color: theme.colors.text1, fontSize: '14px', fontWeight: 700, textDecoration: 'none', @@ -1518,27 +1576,27 @@ export const ghostButton = style({ }) export const subLink = style({ - color: '#666666', + color: theme.colors.text3, fontSize: '14px', fontWeight: 700, textDecoration: 'none', width: 'fit-content', selectors: { '&:hover': { - color: '#111111', + color: theme.colors.text1, }, '&:focus-visible': focusRing, }, }) export const heroSubLink = style({ - color: '#666666', + color: theme.colors.text3, fontSize: '14px', fontWeight: 700, textDecoration: 'none', selectors: { '&:hover': { - color: '#111111', + color: theme.colors.text1, }, '&:focus-visible': focusRing, }, @@ -1547,5 +1605,68 @@ export const heroSubLink = style({ export const helperText = style({ fontSize: '14px', lineHeight: 1.55, - color: '#5B6477', + color: theme.colors.text3, +}) + +export const governanceCopy = style({ + maxWidth: '720px', + fontSize: '17px', + lineHeight: 1.6, + color: theme.colors.text3, +}) + +export const governanceLink = style({ + color: theme.colors.accent, + textDecoration: 'underline', +}) + +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${sectionTitle}, ${heroTitle}, ${montageValue}, ${heroFooterValue}, ${statValue}, ${tabButton}, ${activeTabButton}, ${mobileStatsHeading}, ${daoName}, ${daoSignalValue}, ${coiningPreviewMark}, ${coiningPreviewTitle}, ${coiningTitle}, ${amountPill}, ${droposalTitle}, ${stepTitle}, ${featureItem}, ${governanceLink}, ${lightButton}, ${ghostButton})`, + { + color: theme.colors.text1, + } +) + +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${sectionCopy}, ${introCopyNoWrap}, ${heroText}, ${heroHighlight}, ${montageBody}, ${statLabel}, ${statDetail}, ${sectionInlineCopy}, ${daoDescription}, ${daoSignalLabel}, ${mutedText}, ${droposalSummary}, ${stepBody}, ${subLink}, ${heroSubLink}, ${helperText}, ${governanceCopy})`, + { + color: theme.colors.text3, + } +) + +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${heroHighlightDot}, ${stepMarker})`, + { + background: theme.colors.text1, + } +) + +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${primaryButton}, ${lightButton})`, + { + background: theme.colors.text1, + color: theme.colors.background1, + borderColor: theme.colors.border, + } +) + +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${secondaryButton}, ${ghostButton})`, + { + background: theme.colors.background1, + color: theme.colors.text1, + borderColor: theme.colors.border, + } +) + +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${daoMiniCard}, ${stepCard})`, + { + background: theme.colors.background2, + } +) + +globalStyle(`html[data-theme-mode="dark"] ${logoMarqueeTrack}::before`, { + background: + 'linear-gradient(90deg, rgba(31, 32, 36, 1) 0%, rgba(31, 32, 36, 0) 10%, rgba(31, 32, 36, 0) 90%, rgba(31, 32, 36, 1) 100%)', }) diff --git a/apps/web/src/modules/about/AboutPage.tsx b/apps/web/src/modules/about/AboutPage.tsx index 27abe0452..a22209557 100644 --- a/apps/web/src/modules/about/AboutPage.tsx +++ b/apps/web/src/modules/about/AboutPage.tsx @@ -6,6 +6,8 @@ import { centeredImage, centeredImageWrap, container, + governanceCopy, + governanceLink, page, section, } from './AboutPage.css' @@ -95,15 +97,7 @@ export const AboutPageView: React.FC = () => { eyebrowText="Governance" title="All made possible thanks to Builder DAO" /> - + This public good tooling and the Nouns Builder Protocol are maintained and governed by{' '} { href="https://nouns.build/dao/base/0xe8af882f2f5c79580230710ac0e2344070099432" rel="noreferrer" target="_blank" - style={{ color: '#2563EB', textDecoration: 'underline' }} + className={governanceLink} > Builder DAO @@ -121,7 +115,7 @@ export const AboutPageView: React.FC = () => { href="https://nouns.build/dao/ethereum/0xdf9b7d26c8fc806b1ae6273684556761ff02d422/vote/66" rel="noreferrer" target="_blank" - style={{ color: '#2563EB', textDecoration: 'underline' }} + className={governanceLink} > here diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx index d3c26794b..8fc090b05 100644 --- a/apps/web/src/modules/about/components/CoiningCard.tsx +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -8,6 +8,10 @@ import { coiningMeta, coiningNetworkBadge, coiningPreview, + coiningPreviewSurfaceA, + coiningPreviewSurfaceB, + coiningPreviewSurfaceC, + coiningPreviewSurfaceD, coiningPreviewMark, coiningPreviewTitle, coiningPreviewTop, @@ -24,6 +28,10 @@ type CoiningCardProps = { export const CoiningCard: React.FC = ({ item }) => { const chainLogoSrc = getChainLogoSrc(item.chainLabel) + const previewSurfaceClass = + [coiningPreviewSurfaceA, coiningPreviewSurfaceB, coiningPreviewSurfaceC, coiningPreviewSurfaceD][ + Number(item.id.replace(/\D/g, '')) % 4 || 0 + ] return ( = ({ item }) => { className={coiningCard} href={item.href} > - + {item.amount} {chainLogoSrc ? ( diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx index 7fec68c5e..dc1370682 100644 --- a/apps/web/src/modules/about/components/DaoCard.tsx +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -6,6 +6,10 @@ import React from 'react' import { cardLink, daoAvatar, + daoAvatarSurfaceA, + daoAvatarSurfaceB, + daoAvatarSurfaceC, + daoAvatarSurfaceD, daoAvatarImage, daoCard, daoChainBadge, @@ -25,14 +29,16 @@ type DaoCardProps = { } export const DaoCard: React.FC = ({ dao }) => { + const avatarSurfaceClass = + [daoAvatarSurfaceA, daoAvatarSurfaceB, daoAvatarSurfaceC, daoAvatarSurfaceD][ + Number(dao.id.replace(/\D/g, '')) % 4 || 0 + ] + return ( - + {dao.imageUrl ? ( = { - Active: { background: '#FFF2BF', color: '#6A5300' }, - Succeeded: { background: '#DDF7E7', color: '#0F5B37' }, - Queued: { background: '#DCE6FF', color: '#1D3F84' }, - Defeated: { background: '#F2F4F7', color: '#4F5B6C' }, - Executed: { background: '#ECE7FF', color: '#44348F' }, - Trending: { background: '#FFE1D7', color: '#7D2E0B' }, - Live: { background: '#DDF7E7', color: '#0F5B37' }, - Recent: { background: '#DCE6FF', color: '#1D3F84' }, +const statusClassByType: Record = { + Active: statusActive, + Succeeded: statusSucceeded, + Queued: statusQueued, + Defeated: statusDefeated, + Executed: statusExecuted, + Trending: statusTrending, + Live: statusLive, + Recent: statusRecent, } type DroposalHighlightsSectionProps = { @@ -65,14 +74,13 @@ export const DroposalHighlightsSection: React.FC > - + {proposal.dao} {showStatusBadge ? ( - + {proposal.status} ) : null} diff --git a/apps/web/src/modules/about/components/EcosystemStatGrid.tsx b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx index 0aab29b56..5e2d3d118 100644 --- a/apps/web/src/modules/about/components/EcosystemStatGrid.tsx +++ b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx @@ -3,6 +3,12 @@ import React from 'react' import { statAccent, + statAccentAuction, + statAccentDao, + statAccentMembers, + statAccentProposal, + statAccentTokens, + statAccentTreasury, statCard, statDetail, statGrid, @@ -16,11 +22,30 @@ type EcosystemStatGridProps = { } export const EcosystemStatGrid: React.FC = ({ stats }) => { + const getAccentClass = (id: string) => { + switch (id) { + case 'daos': + return statAccentDao + case 'treasury': + return statAccentTreasury + case 'auctions': + return statAccentAuction + case 'proposals': + return statAccentProposal + case 'members': + return statAccentMembers + case 'tokens': + return statAccentTokens + default: + return statAccentDao + } + } + return ( {stats.map((stat) => ( - + {stat.icon} {stat.label} diff --git a/apps/web/src/modules/about/data.ts b/apps/web/src/modules/about/data.ts index 30dd8a236..d32c4826d 100644 --- a/apps/web/src/modules/about/data.ts +++ b/apps/web/src/modules/about/data.ts @@ -19,8 +19,6 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$1.8M market cap', href: '/explore?search=studio%20nouns', eyebrow: 'DAO paired coin', - accent: '#7D53FF', - surface: 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)', previewLabel: 'Creator pair', }, { @@ -32,8 +30,6 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$940K market cap', href: '/explore?search=camp%20nouns', eyebrow: 'DAO paired coin', - accent: '#FF8A3D', - surface: 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)', previewLabel: 'Content pair', }, { @@ -45,8 +41,6 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$1.2M market cap', href: '/explore?search=builderdao', eyebrow: 'DAO paired coin', - accent: '#0DBA80', - surface: 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)', previewLabel: 'Onchain market', }, { @@ -58,8 +52,6 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$680K market cap', href: '/explore?search=nouns%20fest', eyebrow: 'DAO paired coin', - accent: '#296BFF', - surface: 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)', previewLabel: 'Content pair', }, ] diff --git a/apps/web/src/modules/about/types.ts b/apps/web/src/modules/about/types.ts index 4dee0cde5..8d987584d 100644 --- a/apps/web/src/modules/about/types.ts +++ b/apps/web/src/modules/about/types.ts @@ -3,7 +3,6 @@ export type AboutStat = { label: string value: string detail: string - accent: string icon: string } @@ -17,8 +16,6 @@ export type AboutDao = { signalValue: string href: string badge: string - surface: string - textAccent: string initials: string imageUrl?: string | null recentAuctionImage?: string | null @@ -36,8 +33,6 @@ export type CoiningHighlight = { amount: string href: string eyebrow: string - accent: string - surface: string previewLabel: string } diff --git a/apps/web/src/pages/api/about/dao-tabs.ts b/apps/web/src/pages/api/about/dao-tabs.ts index 80e81a5b5..203f1cbd4 100644 --- a/apps/web/src/pages/api/about/dao-tabs.ts +++ b/apps/web/src/pages/api/about/dao-tabs.ts @@ -126,9 +126,6 @@ const getInitials = (name: string) => .map((part) => part[0]?.toUpperCase() ?? '') .join('') -const getSurface = (index: number) => - ['#EFF6FF', '#DBEAFE', '#BFDBFE', '#E0F2FE'][index % 4] - const FEATURED_DAO_CONFIG: readonly FeaturedDaoConfigItem[] = [ { name: 'Builder DAO', @@ -232,7 +229,7 @@ const createDaoDescriptionResolver = () => { const mapDao = ( dao: DaoQueryItem, chainId: CHAIN_ID, - index: number, + _index: number, signalLabel: string, signalValue: string, badge: string, @@ -249,8 +246,6 @@ const mapDao = ( signalValue, href: buildDaoHref(chainId, dao.tokenAddress), badge, - surface: getSurface(index), - textAccent: '#1E3A8A', initials: getInitials(dao.name || 'DAO'), imageUrl: dao.contractImage || dao.latestToken[0]?.image || null, recentAuctionImage: @@ -358,8 +353,6 @@ const buildFeaturedDaos = async ( signalValue: item.signalValue, href: '/explore', badge: 'Featured', - surface: getSurface(index), - textAccent: '#1E3A8A', initials: getInitials(item.name), imageUrl: null, recentAuctionImage: null, @@ -505,8 +498,6 @@ const buildActiveDaos = async ( signalValue: `${Math.max(1, Math.round((Number(item.dao.endTime) - now) / 3600))}h left`, href: buildDaoHref(item.chainId, item.dao.dao.tokenAddress), badge: 'Active', - surface: getSurface(index), - textAccent: '#1E3A8A', initials: getInitials(item.dao.dao.name || item.dao.token?.name || 'DAO'), imageUrl: matchingDao?.dao.contractImage || item.dao.token?.image || null, recentAuctionImage: item.dao.token?.image || null, diff --git a/apps/web/src/pages/api/about/showcase.ts b/apps/web/src/pages/api/about/showcase.ts index 06533c674..63f63db14 100644 --- a/apps/web/src/pages/api/about/showcase.ts +++ b/apps/web/src/pages/api/about/showcase.ts @@ -150,13 +150,6 @@ const ABOUT_SHOWCASE_QUERY = gql` } ` -const previewSurfaces = [ - 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)', - 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)', - 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)', - 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)', -] - const compactVotes = (votes: number) => `${new Intl.NumberFormat('en-US', { notation: 'compact', @@ -271,7 +264,7 @@ const buildCoiningHighlights = async ( ) .sort((a, b) => b.marketCapUsd - a.marketCapUsd) .slice(0, 4) - .map(({ coin, marketCapUsd }, index) => ({ + .map(({ coin, marketCapUsd }) => ({ id: `${coin.chainId}-${coin.id}`, title: coin.name, creator: walletSnippet(coin.caller as `0x${string}`), @@ -281,8 +274,6 @@ const buildCoiningHighlights = async ( amount: compactMarketCap(marketCapUsd), href: `/coin/${chainIdToSlug(coin.chainId)}/${coin.coinAddress}`, eyebrow: 'DAO paired coin', - accent: '#2563EB', - surface: previewSurfaces[index % previewSurfaces.length], previewLabel: coin.symbol || 'Content coin', })) } diff --git a/apps/web/src/pages/api/about/snapshot.ts b/apps/web/src/pages/api/about/snapshot.ts index ee270aaa9..47090372a 100644 --- a/apps/web/src/pages/api/about/snapshot.ts +++ b/apps/web/src/pages/api/about/snapshot.ts @@ -186,7 +186,6 @@ const buildSnapshotResponse = async (): Promise => { label: 'DAOs launched', value: compactNumber(totalDaos), detail: 'Live DAO count indexed across Builder-supported public networks.', - accent: '#2563EB', icon: '🚀', }, { @@ -195,7 +194,6 @@ const buildSnapshotResponse = async (): Promise => { value: compactEth(totalAuctionSales), detail: 'Cumulative native-token auction sales flowing into community treasuries.', - accent: '#2563EB', icon: '💰', }, { @@ -203,7 +201,6 @@ const buildSnapshotResponse = async (): Promise => { label: 'Active auctions', value: compactNumber(activeAuctions), detail: 'Current live auctions still accepting bids across the ecosystem.', - accent: '#2563EB', icon: '⏰', }, { @@ -211,7 +208,6 @@ const buildSnapshotResponse = async (): Promise => { label: 'Governance proposals', value: compactNumber(totalProposals), detail: 'Total proposals created across DAOs using Builder governance.', - accent: '#2563EB', icon: '📜', }, { @@ -219,7 +215,6 @@ const buildSnapshotResponse = async (): Promise => { label: 'Members holding tokens', value: compactNumber(totalMembers), detail: 'Distinct DAO token holders participating across Builder communities.', - accent: '#2563EB', icon: '👥', }, { @@ -227,7 +222,6 @@ const buildSnapshotResponse = async (): Promise => { label: 'Tokens auctioned', value: compactNumber(totalTokens), detail: 'Member tokens minted and distributed through recurring auctions.', - accent: '#2563EB', icon: '🔥', }, ], From 7daf5b3e9cfff02205f964f07e53ca01e28f97e0 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Sat, 9 May 2026 10:22:24 +0530 Subject: [PATCH 07/13] feat: unify about page actions and proposal status styling --- apps/web/src/modules/about/AboutPage.css.ts | 153 +++++------------- .../about/components/AboutFinalCta.tsx | 23 +-- .../modules/about/components/CoiningCard.tsx | 10 +- .../src/modules/about/components/DaoCard.tsx | 12 +- .../components/DroposalHighlightsSection.tsx | 38 ++--- .../about/components/EcosystemStatGrid.tsx | 4 +- apps/web/src/modules/about/data.ts | 4 + apps/web/src/modules/about/types.ts | 1 + .../modules/dashboard/CreateActions.css.ts | 16 -- .../src/modules/dashboard/CreateActions.tsx | 11 +- apps/web/src/pages/api/about/dao-tabs.ts | 2 +- apps/web/src/pages/api/about/showcase.ts | 10 +- .../ProposalStatus/ProposalStatus.helper.ts | 85 ++++++---- .../src/components/ProposalStatus/index.ts | 1 + packages/proposal-ui/src/index.ts | 1 + packages/zord/src/elements/Button.css.ts | 27 +++- packages/zord/src/elements/Button.tsx | 1 + 17 files changed, 174 insertions(+), 225 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index 05cb7ff68..baa737463 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -550,9 +550,9 @@ export const statAccent = style({ export const statAccentDao = style({ background: theme.colors.background2 }) export const statAccentTreasury = style({ background: theme.colors.positive }) export const statAccentAuction = style({ background: theme.colors.warning }) -export const statAccentProposal = style({ background: theme.colors.accent }) +export const statAccentProposal = style({ background: theme.colors.focusRing }) export const statAccentMembers = style({ background: theme.colors.negative }) -export const statAccentTokens = style({ background: theme.colors.accent }) +export const statAccentTokens = style({ background: theme.colors.focusRing }) export const statLabel = style({ position: 'relative', @@ -806,7 +806,7 @@ export const daoAvatarSurfaceA = style({ color: theme.colors.text1, }) export const daoAvatarSurfaceB = style({ - background: theme.colors.accent, + background: `color-mix(in srgb, ${theme.colors.focusRing} 22%, ${theme.colors.background1})`, color: theme.colors.text1, }) export const daoAvatarSurfaceC = style({ @@ -1103,38 +1103,16 @@ export const statusBadge = style({ justifyContent: 'center', minWidth: '88px', borderRadius: '999px', + border: standardBorderThin, padding: '7px 12px', fontSize: '12px', fontWeight: 700, -}) - -export const statusLive = style({ background: theme.colors.positive, color: theme.colors.text1 }) -export const statusRecent = style({ background: theme.colors.accent, color: theme.colors.text1 }) -export const statusActive = style({ background: theme.colors.warning, color: theme.colors.text1 }) -export const statusSucceeded = style({ - background: theme.colors.positive, - color: theme.colors.text1, -}) -export const statusQueued = style({ - background: theme.colors.background2, - color: theme.colors.text1, -}) -export const statusDefeated = style({ - background: theme.colors.background2, - color: theme.colors.text2, -}) -export const statusExecuted = style({ - background: theme.colors.background2, - color: theme.colors.text1, -}) -export const statusTrending = style({ - background: theme.colors.negative, - color: theme.colors.text1, + background: 'transparent', }) export const daoBadge = style({ - background: theme.colors.background2, - color: theme.colors.text2, + background: '#F7F3E8', + color: '#4F4738', }) export const droposalTitle = style({ @@ -1203,7 +1181,7 @@ export const stepMarker = style({ borderRadius: '10px', alignItems: 'center', justifyContent: 'center', - background: theme.colors.accent, + background: theme.colors.focusRing, border: 'none', color: theme.colors.background1, fontFamily: 'ptRoot, sans-serif', @@ -1520,61 +1498,6 @@ export const finalActions = style({ }, }) -export const lightButton = style({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - minHeight: '42px', - padding: '10px 16px', - borderRadius: '999px', - border: standardBorderThin, - background: theme.colors.text1, - color: theme.colors.background1, - fontSize: '14px', - fontWeight: 700, - textDecoration: 'none', - transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', - selectors: { - '&:hover': { - borderColor: theme.colors.border, - backgroundColor: theme.colors.text2, - color: theme.colors.background1, - }, - '&:focus-visible': focusRing, - }, -}) - -export const finalPrimaryButton = style([ - lightButton, - { - borderColor: theme.colors.border, - }, -]) - -export const ghostButton = style({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - minHeight: '42px', - padding: '10px 16px', - borderRadius: '999px', - background: theme.colors.background1, - color: theme.colors.text1, - fontSize: '14px', - fontWeight: 700, - textDecoration: 'none', - border: standardBorderThin, - transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', - selectors: { - '&:hover': { - cursor: 'pointer', - backgroundColor: softBlueBackground, - borderColor: softBlueBorder, - }, - '&:focus-visible': focusRing, - }, -}) - export const subLink = style({ color: theme.colors.text3, fontSize: '14px', @@ -1616,12 +1539,12 @@ export const governanceCopy = style({ }) export const governanceLink = style({ - color: theme.colors.accent, + color: theme.colors.focusRing, textDecoration: 'underline', }) globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${sectionTitle}, ${heroTitle}, ${montageValue}, ${heroFooterValue}, ${statValue}, ${tabButton}, ${activeTabButton}, ${mobileStatsHeading}, ${daoName}, ${daoSignalValue}, ${coiningPreviewMark}, ${coiningPreviewTitle}, ${coiningTitle}, ${amountPill}, ${droposalTitle}, ${stepTitle}, ${featureItem}, ${governanceLink}, ${lightButton}, ${ghostButton})`, + `html[data-theme-mode="dark"] ${page} :is(${sectionTitle}, ${heroTitle}, ${montageValue}, ${heroFooterValue}, ${statValue}, ${tabButton}, ${activeTabButton}, ${mobileStatsHeading}, ${daoName}, ${daoSignalValue}, ${coiningPreviewMark}, ${coiningPreviewTitle}, ${coiningTitle}, ${amountPill}, ${droposalTitle}, ${stepTitle}, ${featureItem}, ${governanceLink})`, { color: theme.colors.text1, } @@ -1634,37 +1557,37 @@ globalStyle( } ) -globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${heroHighlightDot}, ${stepMarker})`, - { - background: theme.colors.text1, - } -) +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${heroHighlightDot})`, { + background: theme.colors.text1, +}) -globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${primaryButton}, ${lightButton})`, - { - background: theme.colors.text1, - color: theme.colors.background1, - borderColor: theme.colors.border, - } -) +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${primaryButton})`, { + background: theme.colors.text1, + color: theme.colors.background1, + borderColor: theme.colors.border, +}) -globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${secondaryButton}, ${ghostButton})`, - { - background: theme.colors.background1, - color: theme.colors.text1, - borderColor: theme.colors.border, - } -) +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${primaryButton}):hover`, { + background: theme.colors.text2, + color: theme.colors.background1, + borderColor: theme.colors.border, +}) -globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${daoMiniCard}, ${stepCard})`, - { - background: theme.colors.background2, - } -) +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${secondaryButton})`, { + background: theme.colors.background1, + color: theme.colors.text1, + borderColor: theme.colors.border, +}) + +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${secondaryButton}):hover`, { + background: softBlueBackground, + color: theme.colors.text1, + borderColor: softBlueBorder, +}) + +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${daoMiniCard}, ${stepCard})`, { + background: theme.colors.background2, +}) globalStyle(`html[data-theme-mode="dark"] ${logoMarqueeTrack}::before`, { background: diff --git a/apps/web/src/modules/about/components/AboutFinalCta.tsx b/apps/web/src/modules/about/components/AboutFinalCta.tsx index 5b5887226..a0fbc6798 100644 --- a/apps/web/src/modules/about/components/AboutFinalCta.tsx +++ b/apps/web/src/modules/about/components/AboutFinalCta.tsx @@ -1,4 +1,4 @@ -import { Box, Text } from '@buildeross/zord' +import { Box, Button, Text } from '@buildeross/zord' import Link from 'next/link' import React from 'react' @@ -11,9 +11,6 @@ import { finalCtaContent, finalCtaGlow, finalCtaTitle, - finalPrimaryButton, - ghostButton, - subLink, } from '../AboutPage.css' export const AboutFinalCta: React.FC = () => { @@ -40,15 +37,21 @@ export const AboutFinalCta: React.FC = () => { - + + + diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx index 8fc090b05..d3c26794b 100644 --- a/apps/web/src/modules/about/components/CoiningCard.tsx +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -8,10 +8,6 @@ import { coiningMeta, coiningNetworkBadge, coiningPreview, - coiningPreviewSurfaceA, - coiningPreviewSurfaceB, - coiningPreviewSurfaceC, - coiningPreviewSurfaceD, coiningPreviewMark, coiningPreviewTitle, coiningPreviewTop, @@ -28,10 +24,6 @@ type CoiningCardProps = { export const CoiningCard: React.FC = ({ item }) => { const chainLogoSrc = getChainLogoSrc(item.chainLabel) - const previewSurfaceClass = - [coiningPreviewSurfaceA, coiningPreviewSurfaceB, coiningPreviewSurfaceC, coiningPreviewSurfaceD][ - Number(item.id.replace(/\D/g, '')) % 4 || 0 - ] return ( = ({ item }) => { className={coiningCard} href={item.href} > - + {item.amount} {chainLogoSrc ? ( diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx index dc1370682..175f5fa7a 100644 --- a/apps/web/src/modules/about/components/DaoCard.tsx +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -6,11 +6,11 @@ import React from 'react' import { cardLink, daoAvatar, + daoAvatarImage, daoAvatarSurfaceA, daoAvatarSurfaceB, daoAvatarSurfaceC, daoAvatarSurfaceD, - daoAvatarImage, daoCard, daoChainBadge, daoChainBadgeImage, @@ -29,10 +29,12 @@ type DaoCardProps = { } export const DaoCard: React.FC = ({ dao }) => { - const avatarSurfaceClass = - [daoAvatarSurfaceA, daoAvatarSurfaceB, daoAvatarSurfaceC, daoAvatarSurfaceD][ - Number(dao.id.replace(/\D/g, '')) % 4 || 0 - ] + const avatarSurfaceClass = [ + daoAvatarSurfaceA, + daoAvatarSurfaceB, + daoAvatarSurfaceC, + daoAvatarSurfaceD, + ][Number(dao.id.replace(/\D/g, '')) % 4 || 0] return ( diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index a32413041..f1df1535b 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -1,3 +1,5 @@ +import { getProposalStateColorStyle } from '@buildeross/proposal-ui' +import { ProposalState } from '@buildeross/sdk/contract' import { Box, Text } from '@buildeross/zord' import Link from 'next/link' import React from 'react' @@ -15,30 +17,25 @@ import { droposalSummary, droposalTitle, mutedText, - statusActive, - statusDefeated, - statusExecuted, - statusLive, - statusQueued, - statusRecent, statusBadge, - statusSucceeded, - statusTrending, } from '../AboutPage.css' import { dropHighlights } from '../data' import { DroposalHighlight } from '../types' import { getChainLogoSrc } from '../utils' import { SectionIntro } from './SectionIntro' -const statusClassByType: Record = { - Active: statusActive, - Succeeded: statusSucceeded, - Queued: statusQueued, - Defeated: statusDefeated, - Executed: statusExecuted, - Trending: statusTrending, - Live: statusLive, - Recent: statusRecent, +const statusStyleByType: Record< + DroposalHighlight['status'], + { borderColor: string; color: string } +> = { + Active: getProposalStateColorStyle(ProposalState.Active), + Succeeded: getProposalStateColorStyle(ProposalState.Succeeded), + Queued: getProposalStateColorStyle(ProposalState.Queued), + Defeated: getProposalStateColorStyle(ProposalState.Defeated), + Executed: getProposalStateColorStyle(ProposalState.Executed), + Trending: getProposalStateColorStyle(ProposalState.Defeated), + Live: getProposalStateColorStyle(ProposalState.Succeeded), + Recent: getProposalStateColorStyle(ProposalState.Active), } type DroposalHighlightsSectionProps = { @@ -74,12 +71,11 @@ export const DroposalHighlightsSection: React.FC > - - {proposal.dao} - + {proposal.dao} {showStatusBadge ? ( {proposal.status} diff --git a/apps/web/src/modules/about/components/EcosystemStatGrid.tsx b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx index 5e2d3d118..fe2acab7d 100644 --- a/apps/web/src/modules/about/components/EcosystemStatGrid.tsx +++ b/apps/web/src/modules/about/components/EcosystemStatGrid.tsx @@ -45,9 +45,7 @@ export const EcosystemStatGrid: React.FC = ({ stats }) = {stats.map((stat) => ( - - {stat.icon} - + {stat.icon} {stat.label} {stat.value} {stat.detail} diff --git a/apps/web/src/modules/about/data.ts b/apps/web/src/modules/about/data.ts index d32c4826d..4e2d20849 100644 --- a/apps/web/src/modules/about/data.ts +++ b/apps/web/src/modules/about/data.ts @@ -19,6 +19,7 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$1.8M market cap', href: '/explore?search=studio%20nouns', eyebrow: 'DAO paired coin', + surface: 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)', previewLabel: 'Creator pair', }, { @@ -30,6 +31,7 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$940K market cap', href: '/explore?search=camp%20nouns', eyebrow: 'DAO paired coin', + surface: 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)', previewLabel: 'Content pair', }, { @@ -41,6 +43,7 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$1.2M market cap', href: '/explore?search=builderdao', eyebrow: 'DAO paired coin', + surface: 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)', previewLabel: 'Onchain market', }, { @@ -52,6 +55,7 @@ export const coiningHighlights: CoiningHighlight[] = [ amount: '$680K market cap', href: '/explore?search=nouns%20fest', eyebrow: 'DAO paired coin', + surface: 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)', previewLabel: 'Content pair', }, ] diff --git a/apps/web/src/modules/about/types.ts b/apps/web/src/modules/about/types.ts index 8d987584d..6798eb298 100644 --- a/apps/web/src/modules/about/types.ts +++ b/apps/web/src/modules/about/types.ts @@ -33,6 +33,7 @@ export type CoiningHighlight = { amount: string href: string eyebrow: string + surface: string previewLabel: string } diff --git a/apps/web/src/modules/dashboard/CreateActions.css.ts b/apps/web/src/modules/dashboard/CreateActions.css.ts index e42d1142f..35328a602 100644 --- a/apps/web/src/modules/dashboard/CreateActions.css.ts +++ b/apps/web/src/modules/dashboard/CreateActions.css.ts @@ -1,8 +1,5 @@ -import { vars } from '@buildeross/zord' import { style } from '@vanilla-extract/css' -const darkButtonHover = vars.color.neutralHover - export const actionButtons = style({ width: '100%', '@media': { @@ -11,16 +8,3 @@ export const actionButtons = style({ }, }, }) - -export const daoButton = style({ - borderColor: `${vars.color.border} !important`, -}) - -export const createPostButton = style({ - selectors: { - 'html[data-theme-mode="dark"] &:hover': { - backgroundColor: `${darkButtonHover} !important`, - borderColor: `${darkButtonHover} !important`, - }, - }, -}) diff --git a/apps/web/src/modules/dashboard/CreateActions.tsx b/apps/web/src/modules/dashboard/CreateActions.tsx index 6bbcd7e6c..689a0b59b 100644 --- a/apps/web/src/modules/dashboard/CreateActions.tsx +++ b/apps/web/src/modules/dashboard/CreateActions.tsx @@ -3,7 +3,7 @@ import { Button, Flex } from '@buildeross/zord' import Link from 'next/link' import React, { useState } from 'react' -import { actionButtons, createPostButton, daoButton } from './CreateActions.css' +import { actionButtons } from './CreateActions.css' import { DaoSelectorModal } from './DaoSelectorModal' export interface CreateActionsProps { @@ -28,12 +28,7 @@ export const CreateActions: React.FC = ({ userAddress }) => <> - - diff --git a/apps/web/src/pages/api/about/dao-tabs.ts b/apps/web/src/pages/api/about/dao-tabs.ts index 203f1cbd4..35f113556 100644 --- a/apps/web/src/pages/api/about/dao-tabs.ts +++ b/apps/web/src/pages/api/about/dao-tabs.ts @@ -476,7 +476,7 @@ const buildActiveDaos = async ( .filter((item) => item.dao.endTime && Number(item.dao.endTime) > now) .sort((a, b) => Number(a.dao.endTime) - Number(b.dao.endTime)) .slice(0, 4) - .map(async (item, index) => { + .map(async (item) => { const matchingDao = daoCandidates.find( (candidate) => candidate.chainId === item.chainId && diff --git a/apps/web/src/pages/api/about/showcase.ts b/apps/web/src/pages/api/about/showcase.ts index 63f63db14..7b8b0e6a0 100644 --- a/apps/web/src/pages/api/about/showcase.ts +++ b/apps/web/src/pages/api/about/showcase.ts @@ -168,6 +168,13 @@ const compactEthAmount = (amountInEth: number) => maximumFractionDigits: 1, }).format(amountInEth)} sold` +const previewSurfaces = [ + 'linear-gradient(135deg, #F9E7FF 0%, #D9EEFF 100%)', + 'linear-gradient(135deg, #FFF0CC 0%, #FFD9BF 100%)', + 'linear-gradient(135deg, #DFFFF0 0%, #D5F8FF 100%)', + 'linear-gradient(135deg, #E2EBFF 0%, #EDE8FF 100%)', +] + const cleanSentence = (value?: string | null) => { const base = value ?.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ') @@ -264,7 +271,7 @@ const buildCoiningHighlights = async ( ) .sort((a, b) => b.marketCapUsd - a.marketCapUsd) .slice(0, 4) - .map(({ coin, marketCapUsd }) => ({ + .map(({ coin, marketCapUsd }, index) => ({ id: `${coin.chainId}-${coin.id}`, title: coin.name, creator: walletSnippet(coin.caller as `0x${string}`), @@ -274,6 +281,7 @@ const buildCoiningHighlights = async ( amount: compactMarketCap(marketCapUsd), href: `/coin/${chainIdToSlug(coin.chainId)}/${coin.coinAddress}`, eyebrow: 'DAO paired coin', + surface: previewSurfaces[index % previewSurfaces.length], previewLabel: coin.symbol || 'Content coin', })) } diff --git a/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts b/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts index 4e39eca17..310591805 100644 --- a/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts +++ b/packages/proposal-ui/src/components/ProposalStatus/ProposalStatus.helper.ts @@ -2,6 +2,55 @@ import { ProposalState } from '@buildeross/sdk/contract' import { fromSeconds } from '@buildeross/utils/helpers' import { theme } from '@buildeross/zord' +export type ProposalStateColorStyle = { + borderColor: string + color: string +} + +export const proposalStateColorStyles: Record = { + [ProposalState.Pending]: { + borderColor: theme.colors.warningDisabled, + color: theme.colors.warning, + }, + [ProposalState.Active]: { + borderColor: theme.colors.focusRing, + color: theme.colors.focusRing, + }, + [ProposalState.Canceled]: { + borderColor: theme.colors.background2, + color: theme.colors.text4, + }, + [ProposalState.Defeated]: { + borderColor: theme.colors.negativeDisabled, + color: theme.colors.negative, + }, + [ProposalState.Succeeded]: { + borderColor: theme.colors.positiveDisabled, + color: theme.colors.positive, + }, + [ProposalState.Queued]: { + borderColor: theme.colors.neutral, + color: theme.colors.secondary, + }, + [ProposalState.Expired]: { + borderColor: theme.colors.background2, + color: theme.colors.text4, + }, + [ProposalState.Executed]: { + borderColor: theme.colors.positiveDisabled, + color: theme.colors.positive, + }, + [ProposalState.Vetoed]: { + borderColor: theme.colors.background2, + color: theme.colors.text4, + }, +} + +export const getProposalStateColorStyle = ( + state: ProposalState +): ProposalStateColorStyle => + proposalStateColorStyles[state] || proposalStateColorStyles[ProposalState.Expired] + export function formatTime( timediff: number, affix: string, @@ -58,39 +107,5 @@ export function parseState(state: ProposalState) { } export function parseBgColor(state: ProposalState) { - switch (state) { - case ProposalState.Pending: - return { - borderColor: theme.colors.warningDisabled, - color: theme.colors.warning, - } - case ProposalState.Active: - return { - borderColor: '#0085FF', - color: '#0085FF', - } - case ProposalState.Succeeded: - return { - borderColor: theme.colors.positiveDisabled, - color: theme.colors.positive, - } - case ProposalState.Defeated: - return { - borderColor: theme.colors.negativeDisabled, - color: theme.colors.negative, - } - case ProposalState.Executed: - return { - borderColor: theme.colors.positiveDisabled, - color: theme.colors.positive, - } - case ProposalState.Queued: - return { - borderColor: theme.colors.neutral, - color: theme.colors.secondary, - } - case ProposalState.Expired: - default: - return { borderColor: theme.colors.background2, color: theme.colors.text4 } - } + return getProposalStateColorStyle(state) } diff --git a/packages/proposal-ui/src/components/ProposalStatus/index.ts b/packages/proposal-ui/src/components/ProposalStatus/index.ts index 6e7bb6159..b91603830 100644 --- a/packages/proposal-ui/src/components/ProposalStatus/index.ts +++ b/packages/proposal-ui/src/components/ProposalStatus/index.ts @@ -1 +1,2 @@ export * from './ProposalStatus' +export * from './ProposalStatus.helper' diff --git a/packages/proposal-ui/src/index.ts b/packages/proposal-ui/src/index.ts index ef98a54ff..698cd7956 100644 --- a/packages/proposal-ui/src/index.ts +++ b/packages/proposal-ui/src/index.ts @@ -1,2 +1,3 @@ export * from './components' +export * from './components/ProposalStatus/ProposalStatus.helper' export * from './constants' diff --git a/packages/zord/src/elements/Button.css.ts b/packages/zord/src/elements/Button.css.ts index a7cf5b51e..55f7ffe18 100644 --- a/packages/zord/src/elements/Button.css.ts +++ b/packages/zord/src/elements/Button.css.ts @@ -116,6 +116,10 @@ export const buttonVariants = { cursor: 'pointer', backgroundColor: vars.color.accentHover, }, + 'html[data-theme-mode="dark"] &:not([disabled]):hover': { + backgroundColor: vars.color.neutralHover, + borderColor: vars.color.neutralHover, + }, }, }, atoms({ @@ -141,6 +145,27 @@ export const buttonVariants = { backgroundColor: 'background2', }), ]), + secondaryOutline: style([ + { + selectors: { + '&[disabled]': { + color: vars.color.onNeutralDisabled, + borderColor: vars.color.neutralDisabled, + backgroundColor: 'transparent', + }, + '&:not([disabled]):hover': { + cursor: 'pointer', + backgroundColor: vars.color.background2, + }, + }, + }, + atoms({ + color: 'primary', + borderColor: 'border', + borderWidth: 'normal', + backgroundColor: 'transparent', + }), + ]), secondaryAccent: style([ { selectors: { @@ -280,7 +305,7 @@ export const buttonVariants = { }, '&:hover, &:not([disabled]):hover': { cursor: 'pointer', - backgroundColor: vars.color.ghostHover, + backgroundColor: vars.color.background2, }, }, }, diff --git a/packages/zord/src/elements/Button.tsx b/packages/zord/src/elements/Button.tsx index 6d4096d2f..456764906 100644 --- a/packages/zord/src/elements/Button.tsx +++ b/packages/zord/src/elements/Button.tsx @@ -36,6 +36,7 @@ export interface ButtonProps extends FlexProps { variant?: | 'primary' | 'secondary' + | 'secondaryOutline' | 'secondaryAccent' | 'positive' | 'destructive' From 2e9a10204ebb985fdef5271c764b2c8e354c5255 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Sat, 9 May 2026 11:03:59 +0530 Subject: [PATCH 08/13] fix: polish about CTA and theme-aware artwork --- apps/web/public/images/about-divider.svg | 48 ------ apps/web/public/noggles-square-dark.svg | 7 + apps/web/public/noggles-square.svg | 7 + apps/web/src/modules/about/AboutPage.css.ts | 148 +++++++++--------- .../about/components/AboutFinalCta.tsx | 7 +- .../modules/about/components/AboutHero.tsx | 32 ++-- .../components/WhatYouCanBuildSection.tsx | 20 ++- apps/web/src/modules/about/data.ts | 2 +- 8 files changed, 139 insertions(+), 132 deletions(-) delete mode 100644 apps/web/public/images/about-divider.svg create mode 100644 apps/web/public/noggles-square-dark.svg create mode 100644 apps/web/public/noggles-square.svg diff --git a/apps/web/public/images/about-divider.svg b/apps/web/public/images/about-divider.svg deleted file mode 100644 index 00941d7ae..000000000 --- a/apps/web/public/images/about-divider.svg +++ /dev/null @@ -1,48 +0,0 @@ - - image - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/web/public/noggles-square-dark.svg b/apps/web/public/noggles-square-dark.svg new file mode 100644 index 000000000..2eae203f8 --- /dev/null +++ b/apps/web/public/noggles-square-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/public/noggles-square.svg b/apps/web/public/noggles-square.svg new file mode 100644 index 000000000..c4979bf04 --- /dev/null +++ b/apps/web/public/noggles-square.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index baa737463..173682b90 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -50,6 +50,11 @@ export const centeredImage = style({ maxWidth: '100%', height: 'auto', display: 'block', + selectors: { + 'html[data-theme-mode="dark"] &': { + filter: 'invert(1)', + }, + }, }) export const heroVennWrap = style({ @@ -69,6 +74,26 @@ export const heroVennImage = style({ display: 'block', }) +const darkModeSelector = 'html[data-theme-mode="dark"] &' + +export const aboutLightOnly = style({ + display: 'block !important', + selectors: { + [darkModeSelector]: { + display: 'none !important', + }, + }, +}) + +export const aboutDarkOnly = style({ + display: 'none !important', + selectors: { + [darkModeSelector]: { + display: 'block !important', + }, + }, +}) + export const section = style({ marginTop: '72px', '@media': { @@ -214,30 +239,6 @@ export const heroHighlightDot = style({ marginTop: '6px', }) -export const primaryButton = style({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - minHeight: '42px', - padding: '10px 16px', - borderRadius: '999px', - border: standardBorderThin, - color: theme.colors.background1, - background: theme.colors.text1, - fontSize: '14px', - fontWeight: 700, - textDecoration: 'none', - transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', - selectors: { - '&:hover': { - borderColor: theme.colors.border, - backgroundColor: theme.colors.text2, - color: theme.colors.background1, - }, - '&:focus-visible': focusRing, - }, -}) - export const heroActions = style({ display: 'flex', flexDirection: 'column', @@ -251,30 +252,6 @@ export const heroActions = style({ }, }) -export const secondaryButton = style({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - minHeight: '42px', - padding: '10px 16px', - borderRadius: '999px', - border: standardBorderThin, - color: theme.colors.text1, - background: theme.colors.background1, - fontSize: '14px', - fontWeight: 700, - textDecoration: 'none', - transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', - selectors: { - '&:hover': { - cursor: 'pointer', - backgroundColor: softBlueBackground, - borderColor: softBlueBorder, - }, - '&:focus-visible': focusRing, - }, -}) - export const heroPanel = style({ position: 'relative', overflow: 'hidden', @@ -963,7 +940,7 @@ export const coiningPreview = style({ minHeight: '180px', borderRadius: '10px', overflow: 'hidden', - padding: '18px', + padding: '16px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', @@ -1014,7 +991,7 @@ export const coiningNetworkBadge = style({ }) export const coiningPreviewTitle = style({ - maxWidth: '10ch', + maxWidth: '12ch', fontFamily: 'ptRoot, sans-serif', fontSize: '26px', lineHeight: 0.95, @@ -1113,6 +1090,7 @@ export const statusBadge = style({ export const daoBadge = style({ background: '#F7F3E8', color: '#4F4738', + borderColor: theme.colors.text1, }) export const droposalTitle = style({ @@ -1498,6 +1476,36 @@ export const finalActions = style({ }, }) +export const aboutCtaButton = style({ + borderRadius: '999px', + transition: 'background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease', + selectors: { + '&:not([disabled]):hover': { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 14%, ${theme.colors.background1})`, + }, + '&:focus-visible': focusRing, + }, +}) + +globalStyle(`${aboutCtaButton}.zord-button-primary:not([disabled]):hover`, { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 82%, ${theme.colors.text1})`, + color: theme.colors.background1, +}) + +globalStyle(`${aboutCtaButton}.zord-button-outline:not([disabled]):hover`, { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 14%, ${theme.colors.background1})`, + color: theme.colors.text1, +}) + +globalStyle(`${aboutCtaButton}.zord-button-ghost:not([disabled]):hover`, { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 16%, ${theme.colors.background1})`, + color: theme.colors.text1, +}) + export const subLink = style({ color: theme.colors.text3, fontSize: '14px', @@ -1544,7 +1552,7 @@ export const governanceLink = style({ }) globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${sectionTitle}, ${heroTitle}, ${montageValue}, ${heroFooterValue}, ${statValue}, ${tabButton}, ${activeTabButton}, ${mobileStatsHeading}, ${daoName}, ${daoSignalValue}, ${coiningPreviewMark}, ${coiningPreviewTitle}, ${coiningTitle}, ${amountPill}, ${droposalTitle}, ${stepTitle}, ${featureItem}, ${governanceLink})`, + `html[data-theme-mode="dark"] ${page} :is(${sectionTitle}, ${heroTitle}, ${montageValue}, ${heroFooterValue}, ${statValue}, ${tabButton}, ${activeTabButton}, ${mobileStatsHeading}, ${daoName}, ${daoSignalValue}, ${coiningTitle}, ${amountPill}, ${droposalTitle}, ${stepTitle}, ${featureItem})`, { color: theme.colors.text1, } @@ -1561,34 +1569,32 @@ globalStyle(`html[data-theme-mode="dark"] ${page} :is(${heroHighlightDot})`, { background: theme.colors.text1, }) -globalStyle(`html[data-theme-mode="dark"] ${page} :is(${primaryButton})`, { - background: theme.colors.text1, - color: theme.colors.background1, - borderColor: theme.colors.border, -}) - -globalStyle(`html[data-theme-mode="dark"] ${page} :is(${primaryButton}):hover`, { - background: theme.colors.text2, - color: theme.colors.background1, - borderColor: theme.colors.border, -}) - -globalStyle(`html[data-theme-mode="dark"] ${page} :is(${secondaryButton})`, { - background: theme.colors.background1, +globalStyle(`html[data-theme-mode="dark"] ${page} ${aboutCtaButton}:not([disabled]):hover`, { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 30%, ${theme.colors.background1})`, color: theme.colors.text1, - borderColor: theme.colors.border, }) -globalStyle(`html[data-theme-mode="dark"] ${page} :is(${secondaryButton}):hover`, { - background: softBlueBackground, - color: theme.colors.text1, - borderColor: softBlueBorder, -}) +globalStyle( + `html[data-theme-mode="dark"] ${page} ${aboutCtaButton}.zord-button-primary:not([disabled]):hover`, + { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 70%, ${theme.colors.text1})`, + color: theme.colors.background1, + } +) globalStyle(`html[data-theme-mode="dark"] ${page} :is(${daoMiniCard}, ${stepCard})`, { background: theme.colors.background2, }) +globalStyle( + `html[data-theme-mode="dark"] ${page} :is(${coiningPreviewTitle})`, + { + color: theme.colors.background1, + } +) + globalStyle(`html[data-theme-mode="dark"] ${logoMarqueeTrack}::before`, { background: 'linear-gradient(90deg, rgba(31, 32, 36, 1) 0%, rgba(31, 32, 36, 0) 10%, rgba(31, 32, 36, 0) 90%, rgba(31, 32, 36, 1) 100%)', diff --git a/apps/web/src/modules/about/components/AboutFinalCta.tsx b/apps/web/src/modules/about/components/AboutFinalCta.tsx index a0fbc6798..2e8aab6ab 100644 --- a/apps/web/src/modules/about/components/AboutFinalCta.tsx +++ b/apps/web/src/modules/about/components/AboutFinalCta.tsx @@ -3,6 +3,7 @@ import Link from 'next/link' import React from 'react' import { + aboutCtaButton, finalActions, finalChecklist, finalChecklistItem, @@ -37,15 +38,17 @@ export const AboutFinalCta: React.FC = () => { - - - + @@ -89,8 +96,15 @@ export const AboutHero: React.FC = ({ heroHighlights, heroLogos + diff --git a/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx b/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx index 83ac218c8..0051a388f 100644 --- a/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx +++ b/apps/web/src/modules/about/components/WhatYouCanBuildSection.tsx @@ -2,6 +2,8 @@ import { Box, Text } from '@buildeross/zord' import React from 'react' import { + aboutDarkOnly, + aboutLightOnly, compactValueCard, compactValueEmoji, compactValueImage, @@ -23,7 +25,23 @@ export const WhatYouCanBuildSection: React.FC = () => { {builderUseCases.map((item) => ( - {item.imageSrc ? ( + {item.id === 'media-collectives' ? ( + <> + + + + ) : item.imageSrc ? ( Date: Sat, 9 May 2026 11:09:36 +0530 Subject: [PATCH 09/13] fix: remove redundant about card CTA labels --- apps/web/src/modules/about/components/CoiningCard.tsx | 4 ---- apps/web/src/modules/about/components/DaoCard.tsx | 5 ----- .../modules/about/components/DroposalHighlightsSection.tsx | 4 ---- 3 files changed, 13 deletions(-) diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx index d3c26794b..0bb53ffa6 100644 --- a/apps/web/src/modules/about/components/CoiningCard.tsx +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -3,7 +3,6 @@ import Link from 'next/link' import React from 'react' import { - cardLink, coiningCard, coiningMeta, coiningNetworkBadge, @@ -55,9 +54,6 @@ export const CoiningCard: React.FC = ({ item }) => { - - View post - ) } diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx index 175f5fa7a..ea6e1e8ff 100644 --- a/apps/web/src/modules/about/components/DaoCard.tsx +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -4,7 +4,6 @@ import Link from 'next/link' import React from 'react' import { - cardLink, daoAvatar, daoAvatarImage, daoAvatarSurfaceA, @@ -74,10 +73,6 @@ export const DaoCard: React.FC = ({ dao }) => { {dao.signalLabel} {dao.signalValue} - - - View DAO - ) } diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index f1df1535b..16baed713 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -6,7 +6,6 @@ import React from 'react' import { badge, - cardLink, daoBadge, daoChainBadge, daoChainBadgeImage, @@ -103,9 +102,6 @@ export const DroposalHighlightsSection: React.FC {proposal.amount} - - {linkLabel} - ))} From 37b7f5ea0da9c739c415e64a8a6b8c50c28c950f Mon Sep 17 00:00:00 2001 From: dan13ram Date: Sat, 9 May 2026 11:18:40 +0530 Subject: [PATCH 10/13] fix: finalize about card content and status polish --- apps/web/package.json | 1 + apps/web/src/modules/about/AboutPage.css.ts | 4 +- .../src/modules/about/components/DaoCard.tsx | 9 ++-- .../components/DroposalHighlightsSection.tsx | 42 +++++++++++++++---- apps/web/src/modules/about/utils.ts | 7 ++++ package.json | 3 ++ pnpm-lock.yaml | 12 ++++++ 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index dc0869166..46c04e555 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -50,6 +50,7 @@ "react": "^19.2.1", "react-dom": "^19.2.1", "react-mde": "^11.5.0", + "remove-markdown": "^0.6.3", "sablier": "^2.0.1", "sharp": "^0.34.2", "swr": "^2.3.3", diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index 173682b90..cfa6f23b9 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -767,7 +767,7 @@ export const daoIdentity = style({ export const daoAvatar = style({ width: '52px', height: '52px', - borderRadius: '18px', + borderRadius: '999px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -798,6 +798,7 @@ export const daoAvatarSurfaceD = style({ export const daoAvatarImage = style({ width: '100%', height: '100%', + borderRadius: 'inherit', objectFit: 'cover', display: 'block', }) @@ -811,6 +812,7 @@ export const daoName = style({ }) export const daoDescription = style({ + marginTop: '6px', fontSize: '14px', lineHeight: 1.6, color: theme.colors.text3, diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx index ea6e1e8ff..ebb216259 100644 --- a/apps/web/src/modules/about/components/DaoCard.tsx +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -1,4 +1,4 @@ -import { FallbackImage, MarkdownDisplay } from '@buildeross/ui' +import { FallbackImage } from '@buildeross/ui' import { Box, Flex, Text } from '@buildeross/zord' import Link from 'next/link' import React from 'react' @@ -22,12 +22,15 @@ import { daoTop, } from '../AboutPage.css' import { AboutDao } from '../types' +import { toPlainText } from '../utils' type DaoCardProps = { dao: AboutDao } export const DaoCard: React.FC = ({ dao }) => { + const plainDescription = toPlainText(dao.description) + const avatarSurfaceClass = [ daoAvatarSurfaceA, daoAvatarSurfaceB, @@ -52,9 +55,7 @@ export const DaoCard: React.FC = ({ dao }) => { {dao.name} - - {dao.description} - + {plainDescription} {dao.chainIcon ? ( diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index 16baed713..579a5ee2f 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -25,16 +25,40 @@ import { SectionIntro } from './SectionIntro' const statusStyleByType: Record< DroposalHighlight['status'], - { borderColor: string; color: string } + { borderColor: string; color: string; backgroundColor: string } > = { - Active: getProposalStateColorStyle(ProposalState.Active), - Succeeded: getProposalStateColorStyle(ProposalState.Succeeded), - Queued: getProposalStateColorStyle(ProposalState.Queued), - Defeated: getProposalStateColorStyle(ProposalState.Defeated), - Executed: getProposalStateColorStyle(ProposalState.Executed), - Trending: getProposalStateColorStyle(ProposalState.Defeated), - Live: getProposalStateColorStyle(ProposalState.Succeeded), - Recent: getProposalStateColorStyle(ProposalState.Active), + Active: { + ...getProposalStateColorStyle(ProposalState.Active), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Active).borderColor} 12%, transparent)`, + }, + Succeeded: { + ...getProposalStateColorStyle(ProposalState.Succeeded), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Succeeded).borderColor} 12%, transparent)`, + }, + Queued: { + ...getProposalStateColorStyle(ProposalState.Queued), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Queued).borderColor} 12%, transparent)`, + }, + Defeated: { + ...getProposalStateColorStyle(ProposalState.Defeated), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Defeated).borderColor} 12%, transparent)`, + }, + Executed: { + ...getProposalStateColorStyle(ProposalState.Executed), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Executed).borderColor} 12%, transparent)`, + }, + Trending: { + ...getProposalStateColorStyle(ProposalState.Defeated), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Defeated).borderColor} 12%, transparent)`, + }, + Live: { + ...getProposalStateColorStyle(ProposalState.Succeeded), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Succeeded).borderColor} 12%, transparent)`, + }, + Recent: { + ...getProposalStateColorStyle(ProposalState.Active), + backgroundColor: `color-mix(in srgb, ${getProposalStateColorStyle(ProposalState.Active).borderColor} 12%, transparent)`, + }, } type DroposalHighlightsSectionProps = { diff --git a/apps/web/src/modules/about/utils.ts b/apps/web/src/modules/about/utils.ts index af9e443bf..1e3d6aa13 100644 --- a/apps/web/src/modules/about/utils.ts +++ b/apps/web/src/modules/about/utils.ts @@ -1,3 +1,5 @@ +import removeMd from 'remove-markdown' + export const getChainLogoSrc = (chainName?: string | null) => { const normalized = chainName?.trim().toLowerCase() @@ -14,3 +16,8 @@ export const getChainLogoSrc = (chainName?: string | null) => { return null } } + +export const toPlainText = (value?: string | null) => + removeMd(value || '') + .replace(/\s+/g, ' ') + .trim() diff --git a/package.json b/package.json index 767376e31..1d89e9710 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,8 @@ "test": "turbo run test", "type-check": "turbo run type-check", "version-packages": "changeset version" + }, + "dependencies": { + "remove-markdown": "^0.6.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 482120c9e..7be9459f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,10 @@ overrides: importers: .: + dependencies: + remove-markdown: + specifier: ^0.6.3 + version: 0.6.3 devDependencies: '@buildeross/eslint-config-custom': specifier: workspace:* @@ -236,6 +240,9 @@ importers: react-mde: specifier: ^11.5.0 version: 11.5.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + remove-markdown: + specifier: ^0.6.3 + version: 0.6.3 sablier: specifier: ^2.0.1 version: 2.0.1(@coral-xyz/anchor@0.30.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10))(viem@2.47.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)) @@ -10641,6 +10648,9 @@ packages: remedial@1.0.8: resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + remove-markdown@0.6.3: + resolution: {integrity: sha512-Qvp2p0Q1irE7AaJO7QemJe04HdObHylJrG+q4hszvPlYp7q4EvfINpEIaIEFdB+3XTDp1h6fiyT60ae00gmRow==} + remove-trailing-separator@1.1.0: resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} @@ -24583,6 +24593,8 @@ snapshots: remedial@1.0.8: {} + remove-markdown@0.6.3: {} + remove-trailing-separator@1.1.0: {} remove-trailing-spaces@1.0.9: {} From ef90afcc2d3a13883f096931ed956f179db04388 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Sat, 9 May 2026 11:27:14 +0530 Subject: [PATCH 11/13] fix: use chain metadata icons across about page --- apps/web/src/modules/about/AboutPage.css.ts | 10 ----- .../modules/about/components/CoiningCard.tsx | 4 +- .../src/modules/about/components/DaoCard.tsx | 7 ++-- .../components/DroposalHighlightsSection.tsx | 12 ++++-- apps/web/src/modules/about/utils.ts | 39 ++++++++++++------- 5 files changed, 39 insertions(+), 33 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index cfa6f23b9..2dad75e22 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -739,15 +739,10 @@ export const daoTop = style({ export const daoChainBadge = style({ width: '34px', height: '34px', - borderRadius: '10px', - border: standardBorderThin, - background: theme.colors.background1, - boxShadow: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, - overflow: 'hidden', }) export const daoChainBadgeImage = style({ @@ -981,15 +976,10 @@ export const coiningPreviewMark = style({ export const coiningNetworkBadge = style({ width: '34px', height: '34px', - borderRadius: '10px', - border: standardBorderThin, - background: theme.colors.background1, - boxShadow: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, - overflow: 'hidden', }) export const coiningPreviewTitle = style({ diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx index 0bb53ffa6..e067e00a0 100644 --- a/apps/web/src/modules/about/components/CoiningCard.tsx +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -42,7 +42,9 @@ export const CoiningCard: React.FC = ({ item }) => { src={chainLogoSrc} /> - ) : null} + ) : ( + {item.chainLabel} + )} {item.previewLabel} diff --git a/apps/web/src/modules/about/components/DaoCard.tsx b/apps/web/src/modules/about/components/DaoCard.tsx index ebb216259..217b0ab3d 100644 --- a/apps/web/src/modules/about/components/DaoCard.tsx +++ b/apps/web/src/modules/about/components/DaoCard.tsx @@ -22,7 +22,7 @@ import { daoTop, } from '../AboutPage.css' import { AboutDao } from '../types' -import { toPlainText } from '../utils' +import { getChainLogoSrc, toPlainText } from '../utils' type DaoCardProps = { dao: AboutDao @@ -30,6 +30,7 @@ type DaoCardProps = { export const DaoCard: React.FC = ({ dao }) => { const plainDescription = toPlainText(dao.description) + const chainLogoSrc = getChainLogoSrc(dao.chainName) || dao.chainIcon const avatarSurfaceClass = [ daoAvatarSurfaceA, @@ -58,13 +59,13 @@ export const DaoCard: React.FC = ({ dao }) => { {plainDescription} - {dao.chainIcon ? ( + {chainLogoSrc ? ( ) : null} diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index 579a5ee2f..7641cec18 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -85,7 +85,10 @@ export const DroposalHighlightsSection: React.FC - {highlights.map((proposal) => ( + {highlights.map((proposal) => { + const chainLogoSrc = getChainLogoSrc(proposal.category) + + return ( {proposal.status} ) : null} - {getChainLogoSrc(proposal.category) ? ( + {chainLogoSrc ? ( ) : ( @@ -128,7 +131,8 @@ export const DroposalHighlightsSection: React.FC {proposal.amount} - ))} + ) + })} ) diff --git a/apps/web/src/modules/about/utils.ts b/apps/web/src/modules/about/utils.ts index 1e3d6aa13..1a0daf629 100644 --- a/apps/web/src/modules/about/utils.ts +++ b/apps/web/src/modules/about/utils.ts @@ -1,21 +1,30 @@ +import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains' import removeMd from 'remove-markdown' -export const getChainLogoSrc = (chainName?: string | null) => { - const normalized = chainName?.trim().toLowerCase() +const normalizeChainLabel = (value?: string | null) => + (value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, ' ') + .trim() + +const CHAIN_ICON_BY_LABEL = new Map( + PUBLIC_DEFAULT_CHAINS.flatMap((chain) => { + const labels = [chain.name, chain.slug].filter(Boolean).map(normalizeChainLabel) + const fallbackLabels = labels.flatMap((label) => { + if (!label.includes('sepolia')) return [] + + return [label.replace(/\s*sepolia$/, '').trim()] + }) + + return [...labels, ...fallbackLabels] + .filter(Boolean) + .map((label) => [label, chain.icon] as const) + }) +) - switch (normalized) { - case 'base': - return '/chains/base.svg' - case 'ethereum': - return '/chains/ethereum.svg' - case 'optimism': - return '/chains/optimism.svg' - case 'zora': - return '/chains/zora.png' - default: - return null - } -} +export const getChainLogoSrc = (chainName?: string | null) => + CHAIN_ICON_BY_LABEL.get(normalizeChainLabel(chainName)) || null export const toPlainText = (value?: string | null) => removeMd(value || '') From 2949a2687006e4d5bb7a6aa0e960733fc4a7ee5c Mon Sep 17 00:00:00 2001 From: dan13ram Date: Sat, 9 May 2026 14:44:34 +0530 Subject: [PATCH 12/13] fix: improve dark/light mode contrast on drop and coin detail pages --- apps/web/src/modules/about/AboutPage.css.ts | 22 ++--- .../about/components/AboutFinalCta.tsx | 16 +++- .../modules/about/components/AboutHero.tsx | 10 ++- .../modules/about/components/CoiningCard.tsx | 1 - .../components/DroposalHighlightsSection.tsx | 80 +++++++++---------- .../coin/CoinDetail/CoinCommentForm.css.ts | 6 +- .../coin/CoinDetail/CoinComments.css.ts | 3 +- .../modules/coin/CoinDetail/CoinComments.tsx | 2 +- .../modules/coin/CoinDetail/CoinDetail.css.ts | 6 ++ .../modules/drop/DropDetail/DropDetail.css.ts | 8 ++ .../modules/drop/DropDetail/DropDetail.tsx | 2 + .../dao-ui/src/components/Gallery/Gallery.tsx | 1 + packages/feed-ui/src/Modals/MintModal.tsx | 1 + .../src/DropMintWidget/DropMintWidget.css.ts | 27 +++++-- .../ui/src/DropMintWidget/DropMintWidget.tsx | 31 ++++--- 15 files changed, 141 insertions(+), 75 deletions(-) diff --git a/apps/web/src/modules/about/AboutPage.css.ts b/apps/web/src/modules/about/AboutPage.css.ts index 2dad75e22..3c9fec2c0 100644 --- a/apps/web/src/modules/about/AboutPage.css.ts +++ b/apps/web/src/modules/about/AboutPage.css.ts @@ -1561,11 +1561,14 @@ globalStyle(`html[data-theme-mode="dark"] ${page} :is(${heroHighlightDot})`, { background: theme.colors.text1, }) -globalStyle(`html[data-theme-mode="dark"] ${page} ${aboutCtaButton}:not([disabled]):hover`, { - borderColor: theme.colors.focusRing, - backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 30%, ${theme.colors.background1})`, - color: theme.colors.text1, -}) +globalStyle( + `html[data-theme-mode="dark"] ${page} ${aboutCtaButton}:not([disabled]):hover`, + { + borderColor: theme.colors.focusRing, + backgroundColor: `color-mix(in srgb, ${theme.colors.focusRing} 30%, ${theme.colors.background1})`, + color: theme.colors.text1, + } +) globalStyle( `html[data-theme-mode="dark"] ${page} ${aboutCtaButton}.zord-button-primary:not([disabled]):hover`, @@ -1580,12 +1583,9 @@ globalStyle(`html[data-theme-mode="dark"] ${page} :is(${daoMiniCard}, ${stepCard background: theme.colors.background2, }) -globalStyle( - `html[data-theme-mode="dark"] ${page} :is(${coiningPreviewTitle})`, - { - color: theme.colors.background1, - } -) +globalStyle(`html[data-theme-mode="dark"] ${page} :is(${coiningPreviewTitle})`, { + color: theme.colors.background1, +}) globalStyle(`html[data-theme-mode="dark"] ${logoMarqueeTrack}::before`, { background: diff --git a/apps/web/src/modules/about/components/AboutFinalCta.tsx b/apps/web/src/modules/about/components/AboutFinalCta.tsx index 2e8aab6ab..253b8843e 100644 --- a/apps/web/src/modules/about/components/AboutFinalCta.tsx +++ b/apps/web/src/modules/about/components/AboutFinalCta.tsx @@ -38,10 +38,22 @@ export const AboutFinalCta: React.FC = () => { - - diff --git a/apps/web/src/modules/about/components/CoiningCard.tsx b/apps/web/src/modules/about/components/CoiningCard.tsx index e067e00a0..f468c13aa 100644 --- a/apps/web/src/modules/about/components/CoiningCard.tsx +++ b/apps/web/src/modules/about/components/CoiningCard.tsx @@ -55,7 +55,6 @@ export const CoiningCard: React.FC = ({ item }) => { By {item.creator} for {item.dao} - ) } diff --git a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx index 7641cec18..30c5d629f 100644 --- a/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx +++ b/apps/web/src/modules/about/components/DroposalHighlightsSection.tsx @@ -89,48 +89,48 @@ export const DroposalHighlightsSection: React.FC const chainLogoSrc = getChainLogoSrc(proposal.category) return ( - - - - {proposal.dao} - {showStatusBadge ? ( - - {proposal.status} - - ) : null} - {chainLogoSrc ? ( - - - - ) : ( - {proposal.category} - )} + + + + {proposal.dao} + {showStatusBadge ? ( + + {proposal.status} + + ) : null} + {chainLogoSrc ? ( + + + + ) : ( + {proposal.category} + )} + + + {proposal.title} + + + {proposal.summary} + - - {proposal.title} - - - {proposal.summary} - - - - {proposal.amount} - - + + {proposal.amount} + + ) })} diff --git a/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts b/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts index c5171a9e3..b9f98c63f 100644 --- a/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts +++ b/apps/web/src/modules/coin/CoinDetail/CoinCommentForm.css.ts @@ -4,7 +4,11 @@ import { style } from '@vanilla-extract/css' export const commentFormContainer = style([ atoms({ w: '100%', - backgroundColor: 'background2', + p: 'x4', + backgroundColor: 'background1', + borderColor: 'border', + borderStyle: 'solid', + borderWidth: 'thin', borderRadius: 'curved', mb: 'x5', }), diff --git a/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts b/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts index 33dd250c7..a55c94cff 100644 --- a/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts +++ b/apps/web/src/modules/coin/CoinDetail/CoinComments.css.ts @@ -18,7 +18,8 @@ export const commentCard = style([ borderRadius: 'curved', borderWidth: 'thin', borderStyle: 'solid', - borderColor: 'borderOnImage', + borderColor: 'border', + backgroundColor: 'background1', py: 'x2', px: 'x4', }), diff --git a/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx b/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx index 5238464ae..ce802c76b 100644 --- a/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx +++ b/apps/web/src/modules/coin/CoinDetail/CoinComments.tsx @@ -56,7 +56,7 @@ const CommentCard: React.FC<{ nameStyle={{ fontSize: '16px' }} mobileTapBehavior="toggle" /> - + {timeAgo} diff --git a/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts b/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts index d3524b51c..240f799e5 100644 --- a/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts +++ b/apps/web/src/modules/coin/CoinDetail/CoinDetail.css.ts @@ -41,6 +41,9 @@ export const coinInfoPanel = style([ p: 'x6', borderRadius: 'curved', backgroundColor: 'background2', + borderColor: 'border', + borderWidth: 'thin', + borderStyle: 'solid', }), ]) @@ -49,6 +52,9 @@ export const swapPanel = style([ p: 'x6', borderRadius: 'curved', backgroundColor: 'background2', + borderColor: 'border', + borderWidth: 'thin', + borderStyle: 'solid', }), { '@media': { diff --git a/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts b/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts index a2251b0a6..309afcc24 100644 --- a/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts +++ b/apps/web/src/modules/drop/DropDetail/DropDetail.css.ts @@ -41,12 +41,20 @@ export const dropInfoPanel = style([ p: 'x6', borderRadius: 'curved', backgroundColor: 'background2', + borderColor: 'border', + borderWidth: 'thin', + borderStyle: 'solid', }), ]) export const mintPanel = style([ atoms({ + p: 'x6', borderRadius: 'curved', + backgroundColor: 'background2', + borderColor: 'border', + borderWidth: 'thin', + borderStyle: 'solid', }), { '@media': { diff --git a/apps/web/src/modules/drop/DropDetail/DropDetail.tsx b/apps/web/src/modules/drop/DropDetail/DropDetail.tsx index 0d584d6bd..6f2638890 100644 --- a/apps/web/src/modules/drop/DropDetail/DropDetail.tsx +++ b/apps/web/src/modules/drop/DropDetail/DropDetail.tsx @@ -122,6 +122,7 @@ export const DropDetail = ({ saleEnd={saleEnd} editionSize={drop.editionSize} maxPerAddress={parseInt(drop.maxSalePurchasePerAddress)} + unstyledContainer /> @@ -167,6 +168,7 @@ export const DropDetail = ({ saleEnd={saleEnd} editionSize={drop.editionSize} maxPerAddress={parseInt(drop.maxSalePurchasePerAddress)} + unstyledContainer /> diff --git a/packages/dao-ui/src/components/Gallery/Gallery.tsx b/packages/dao-ui/src/components/Gallery/Gallery.tsx index 2a5b515ca..f1bbaa36c 100644 --- a/packages/dao-ui/src/components/Gallery/Gallery.tsx +++ b/packages/dao-ui/src/components/Gallery/Gallery.tsx @@ -127,6 +127,7 @@ const MintWidgetModal: React.FC = ({ editionSize={drop.editionSize} maxPerAddress={parseInt(drop.maxSalePurchasePerAddress)} onMintSuccess={handleCloseModal} + unstyledContainer /> diff --git a/packages/feed-ui/src/Modals/MintModal.tsx b/packages/feed-ui/src/Modals/MintModal.tsx index d0b1eb67c..88d1d06a3 100644 --- a/packages/feed-ui/src/Modals/MintModal.tsx +++ b/packages/feed-ui/src/Modals/MintModal.tsx @@ -74,6 +74,7 @@ export const MintModal: React.FC = ({ editionSize={editionSize} maxPerAddress={maxPerAddress} onMintSuccess={onSuccessMint} + unstyledContainer /> diff --git a/packages/ui/src/DropMintWidget/DropMintWidget.css.ts b/packages/ui/src/DropMintWidget/DropMintWidget.css.ts index de6c1767f..b17bcc65c 100644 --- a/packages/ui/src/DropMintWidget/DropMintWidget.css.ts +++ b/packages/ui/src/DropMintWidget/DropMintWidget.css.ts @@ -5,7 +5,17 @@ export const widgetContainer = style([ atoms({ p: 'x6', borderRadius: 'phat', - backgroundColor: 'background2', + backgroundColor: 'background1', + borderColor: 'border', + borderStyle: 'solid', + borderWidth: 'thin', + }), +]) + +export const widgetContainerUnstyled = style([ + atoms({ + p: 'x0', + borderRadius: 'curved', }), ]) @@ -20,8 +30,8 @@ export const mintInputContainer = style([ export const mintInput = style({ fontSize: '1.125rem', fontWeight: 600, - border: '2px solid transparent', - backgroundColor: 'transparent', + border: `2px solid ${vars.color.background1}`, + backgroundColor: vars.color.background2, textAlign: 'center', width: '100%', flex: 1, @@ -34,6 +44,10 @@ export const mintInput = style({ borderColor: vars.color.border, backgroundColor: vars.color.background1, }, + '&:focus-visible': { + outline: `2px solid ${vars.color.focusRing}`, + outlineOffset: '2px', + }, '&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': { WebkitAppearance: 'none', margin: 0, @@ -46,7 +60,7 @@ export const mintInput = style({ export const quantityInputWrapper = style([ atoms({ - backgroundColor: 'background1', + backgroundColor: 'transparent', borderRadius: 'curved', p: 'x2', }), @@ -78,7 +92,10 @@ export const quantityButton = style({ selectors: { '&:disabled': { cursor: 'not-allowed', - opacity: 0.4, + opacity: 1, + color: vars.color.text3, + borderColor: vars.color.border, + backgroundColor: vars.color.background1, }, }, }) diff --git a/packages/ui/src/DropMintWidget/DropMintWidget.tsx b/packages/ui/src/DropMintWidget/DropMintWidget.tsx index bdcc197ea..f13b7f64b 100644 --- a/packages/ui/src/DropMintWidget/DropMintWidget.tsx +++ b/packages/ui/src/DropMintWidget/DropMintWidget.tsx @@ -17,6 +17,7 @@ import { quantityInputWrapper, successMessage, widgetContainer, + widgetContainerUnstyled, } from './DropMintWidget.css' export interface DropMintWidgetProps { @@ -32,6 +33,7 @@ export interface DropMintWidgetProps { editionSize?: string maxPerAddress?: number onMintSuccess?: (txHash: string) => void + unstyledContainer?: boolean } export const DropMintWidget = ({ @@ -47,6 +49,7 @@ export const DropMintWidget = ({ // editionSize, maxPerAddress, onMintSuccess, + unstyledContainer = false, }: DropMintWidgetProps) => { const [quantity, setQuantity] = useState(1) const [comment, setComment] = useState('') @@ -119,7 +122,10 @@ export const DropMintWidget = ({ } return ( - + {/* Price */} @@ -170,15 +176,18 @@ export const DropMintWidget = ({ Quantity - + {quantity > 1 ? ( + + ) : ( + + )} - - - + >>['daos'][number] } +type SettledChainResult = { + chainId: CHAIN_ID + value: T +} + +const getFulfilledChainResults = ( + settledResults: PromiseSettledResult>[], + context: string +) => + settledResults.flatMap((result) => { + if (result.status === 'fulfilled') { + return [result.value.value] + } + + const reason = result.reason as { chainId?: CHAIN_ID; error?: unknown } + console.error( + `About dao tabs ${context} failed for chain ${String(reason?.chainId ?? 'unknown')}:`, + reason?.error || reason + ) + return [] + }) + type FeaturedDaoConfigItem = { name: string aliases?: readonly string[] @@ -293,49 +315,76 @@ const fetchDaoCandidates = async ( const fetchCrossChainDaoCandidates = async ( options: { totalAuctionSalesGt?: string; first?: number } = {} ): Promise => { - const results = await Promise.all( + const settledResults = await Promise.allSettled( PUBLIC_DEFAULT_CHAINS.map(async (chain) => { - const daos = await fetchDaoCandidates(chain.id, options) + try { + const daos = await fetchDaoCandidates(chain.id, options) - return daos.map((dao) => ({ chainId: chain.id, dao })) + return { + chainId: chain.id, + value: daos.map((dao) => ({ chainId: chain.id, dao })), + } + } catch (error) { + throw { chainId: chain.id, error } + } }) ) - return results.flat() + return getFulfilledChainResults(settledResults, 'candidate fetch').flat() } const findFeaturedMatch = (targets: string[], candidates: CrossChainDaoCandidate[]) => candidates.find((item) => isNameMatch(targets, item.dao.name)) const fetchFeaturedMatch = async (targets: string[]) => { - const results = await Promise.all( + const settledResults = await Promise.allSettled( PUBLIC_DEFAULT_CHAINS.flatMap((chain) => targets.map(async (target) => { - const daos = await fetchDaoCandidates(chain.id, { - first: 25, - nameContains: target, - }) - - return daos.map((dao) => ({ chainId: chain.id, dao })) + try { + const daos = await fetchDaoCandidates(chain.id, { + first: 25, + nameContains: target, + }) + + return { + chainId: chain.id, + value: daos.map((dao) => ({ chainId: chain.id, dao })), + } + } catch (error) { + throw { chainId: chain.id, error } + } }) ) ) - return findFeaturedMatch(targets, results.flat()) + return findFeaturedMatch( + targets, + getFulfilledChainResults(settledResults, 'featured match fetch').flat() + ) } const buildFeaturedDaos = async ( resolveDescription: ReturnType ): Promise => { - const results = await Promise.all( + const settledResults = await Promise.allSettled( PUBLIC_DEFAULT_CHAINS.map(async (chain) => { - const daos = await fetchDaoCandidates(chain.id, { first: 800 }) + try { + const daos = await fetchDaoCandidates(chain.id, { first: 800 }) - return daos.map((dao) => ({ chainId: chain.id, dao })) + return { + chainId: chain.id, + value: daos.map((dao) => ({ chainId: chain.id, dao })), + } + } catch (error) { + throw { chainId: chain.id, error } + } }) ) - const featuredCandidates = results.flat() + const featuredCandidates = getFulfilledChainResults( + settledResults, + 'featured candidate fetch' + ).flat() return Promise.all( FEATURED_DAO_CONFIG.map(async (item, index) => { @@ -458,18 +507,28 @@ const buildActiveDaos = async ( resolveDescription: ReturnType ) => { const now = Math.floor(Date.now() / 1000) - const activeResults = await Promise.all( + const settledResults = await Promise.allSettled( PUBLIC_DEFAULT_CHAINS.map(async (chain) => { - const result = await exploreDaosRequest(chain.id, 12, 0, Auction_OrderBy.EndTime) + try { + const result = await exploreDaosRequest(chain.id, 12, 0, Auction_OrderBy.EndTime) - return (result?.daos ?? []).map((dao) => ({ - chainId: chain.id, - dao, - })) + return { + chainId: chain.id, + value: (result?.daos ?? []).map((dao) => ({ + chainId: chain.id, + dao, + })), + } + } catch (error) { + throw { chainId: chain.id, error } + } }) ) - const activeDaos = activeResults.flat() as CrossChainActiveDao[] + const activeDaos = getFulfilledChainResults( + settledResults, + 'active dao fetch' + ).flat() as CrossChainActiveDao[] return Promise.all( activeDaos diff --git a/apps/web/src/pages/api/about/showcase.ts b/apps/web/src/pages/api/about/showcase.ts index 7b8b0e6a0..a67f5d49a 100644 --- a/apps/web/src/pages/api/about/showcase.ts +++ b/apps/web/src/pages/api/about/showcase.ts @@ -291,7 +291,11 @@ const buildDropHighlights = (drops: CrossChainDrop[]): DroposalHighlight[] => { return drops .filter((drop) => drop.dao?.tokenAddress && drop.name) - .sort((a, b) => Number(b.totalSalesAmount || '0') - Number(a.totalSalesAmount || '0')) + .sort((a, b) => { + const aAmount = BigInt(a.totalSalesAmount || '0') + const bAmount = BigInt(b.totalSalesAmount || '0') + return bAmount > aAmount ? 1 : bAmount < aAmount ? -1 : 0 + }) .slice(0, 5) .map((drop) => ({ id: `${drop.chainId}-${drop.id}`, @@ -370,9 +374,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: 'Method not allowed' }) } + const { maxAge, swr } = CACHE_TIMES.EXPLORE + try { const payload = await buildShowcaseResponse() - const { maxAge, swr } = CACHE_TIMES.EXPLORE res.setHeader( 'Cache-Control', @@ -389,6 +394,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } catch (error) { console.error('About showcase error:', error) + res.setHeader( + 'Cache-Control', + `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}` + ) + return res.status(200).json({ coining: fallbackCoiningHighlights, drops: fallbackDropHighlights, diff --git a/apps/web/src/pages/api/about/snapshot.ts b/apps/web/src/pages/api/about/snapshot.ts index 47090372a..5a6822093 100644 --- a/apps/web/src/pages/api/about/snapshot.ts +++ b/apps/web/src/pages/api/about/snapshot.ts @@ -88,6 +88,10 @@ const fetchChainSnapshot = async (subgraphUrl: string): Promise = return data.daos ?? [] } +// TODO: This full scan of daoTokenOwners via paginated queries runs on every cache miss, +// causing unbounded latency on the public API. Move this logic to a background job/cron that +// periodically precomputes unique owner sets, then have the handler read that cached result. +// Not done now because it requires a separate background process + persistence layer. const fetchUniqueOwnersForChain = async (subgraphUrl: string): Promise> => { const client = new GraphQLClient(subgraphUrl, { headers: { 'Content-Type': 'application/json' }, @@ -174,9 +178,13 @@ const buildSnapshotResponse = async (): Promise => { } } - const totalMembers = - uniqueOwners.size || - daos.reduce((total, dao) => total + Number(dao.ownerCount || 0), 0) + const allOwnerScansSucceeded = chainSnapshots.every((snapshot) => snapshot.uniqueOwners) + const fallbackTotalMembers = daos.reduce( + (total, dao) => total + Number(dao.ownerCount || 0), + 0 + ) + + const totalMembers = allOwnerScansSucceeded ? uniqueOwners.size : fallbackTotalMembers const totalTokens = daos.reduce((total, dao) => total + Number(dao.totalSupply || 0), 0) return { diff --git a/apps/web/src/pages/dao/[network]/[token]/index.tsx b/apps/web/src/pages/dao/[network]/[token]/index.tsx index d199b67c6..36346da41 100644 --- a/apps/web/src/pages/dao/[network]/[token]/index.tsx +++ b/apps/web/src/pages/dao/[network]/[token]/index.tsx @@ -44,6 +44,8 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress const { addresses } = useDaoStore() const chain = useChainStore((x) => x.chain) const chainIdKey = chain.id as keyof typeof MERKLE_RESERVE_MINTER + const merkleMinter = MERKLE_RESERVE_MINTER[chainIdKey] + const redeemMinter = ERC721_REDEEM_MINTER[chainIdKey] const auctionContractParams = { abi: auctionAbi, @@ -57,26 +59,40 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress chainId: chain.id, } + const contracts = [ + { ...auctionContractParams, functionName: 'owner' as const }, + { ...tokenContractParams, functionName: 'remainingTokensInReserve' as const }, + ...(merkleMinter + ? ([ + { + ...tokenContractParams, + functionName: 'minter' as const, + args: [merkleMinter] as const, + }, + ] as const) + : []), + ...(redeemMinter + ? ([ + { + ...tokenContractParams, + functionName: 'minter' as const, + args: [redeemMinter] as const, + }, + ] as const) + : []), + ] + const { data: contractData } = useReadContracts({ allowFailure: false, - contracts: [ - { ...auctionContractParams, functionName: 'owner' }, - { ...tokenContractParams, functionName: 'remainingTokensInReserve' }, - { - ...tokenContractParams, - functionName: 'minter', - args: [MERKLE_RESERVE_MINTER[chainIdKey]], - }, - { - ...tokenContractParams, - functionName: 'minter', - args: [ERC721_REDEEM_MINTER[chainIdKey]], - }, - ] as const, + contracts, }) - const [owner, remainingTokensInReserve, isMerkleReserveMinter, isERC721RedeemMinter] = - unpackOptionalArray(contractData, 4) + const [owner, remainingTokensInReserve, ...minterStatus] = (unpackOptionalArray( + contractData, + contracts.length + ) ?? []) as [unknown, bigint | undefined, ...unknown[]] + const isMerkleReserveMinter = merkleMinter ? !!minterStatus[0] : false + const isERC721RedeemMinter = redeemMinter ? !!minterStatus[merkleMinter ? 1 : 0] : false // Separate read for signer minter status const { data: isSignerMinter } = useReadContract({ @@ -113,13 +129,13 @@ const DaoPage: NextPageWithLayout = ({ chainId, collectionAddress const handleMinterEnabled = React.useCallback( async (minterAddress: AddressType) => { // Navigate to appropriate tab when minter is enabled - if (minterAddress === MERKLE_RESERVE_MINTER[chainIdKey]) { + if (merkleMinter && minterAddress === merkleMinter) { await openTab('merkle-reserve') - } else if (minterAddress === ERC721_REDEEM_MINTER[chainIdKey]) { + } else if (redeemMinter && minterAddress === redeemMinter) { await openTab('erc721-redeem') } }, - [chainIdKey, openTab] + [merkleMinter, redeemMinter, openTab] ) const openTokenPage = React.useCallback( diff --git a/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx b/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx index b7a36d4c3..9f9fe04c9 100644 --- a/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx +++ b/apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx @@ -51,6 +51,7 @@ import { notFoundWrap } from 'src/styles/404.css' import * as styles from 'src/styles/create.css' import { getAddress, isAddress, isAddressEqual } from 'viem' import { useAccount, useReadContract } from 'wagmi' +import { useShallow } from 'zustand/shallow' const createSelectOption = (type: TransactionType) => ({ value: type, @@ -165,7 +166,25 @@ const CreateProposalPage: NextPageWithLayout = () => { setDiscussionUrl, setRepresentedAddressEnabled, clearProposal, - } = useProposalStore() + } = useProposalStore( + useShallow((state) => ({ + transactionType: state.transactionType, + setTransactionType: state.setTransactionType, + resetTransactionType: state.resetTransactionType, + transactions: state.transactions, + title: state.title, + summary: state.summary, + representedAddress: state.representedAddress, + discussionUrl: state.discussionUrl, + representedAddressEnabled: state.representedAddressEnabled, + setTitle: state.setTitle, + setSummary: state.setSummary, + setRepresentedAddress: state.setRepresentedAddress, + setDiscussionUrl: state.setDiscussionUrl, + setRepresentedAddressEnabled: state.setRepresentedAddressEnabled, + clearProposal: state.clearProposal, + })) + ) const initialStageFromQuery = query?.stage === 'transactions' ? 'transactions' : 'draft' const [createStage, setCreateStage] = React.useState<'draft' | 'transactions'>(() => diff --git a/apps/web/src/utils/api/ai/summaries.ts b/apps/web/src/utils/api/ai/summaries.ts index e7b063e3a..c8c3fb785 100644 --- a/apps/web/src/utils/api/ai/summaries.ts +++ b/apps/web/src/utils/api/ai/summaries.ts @@ -1,10 +1,10 @@ import { gateway } from '@ai-sdk/gateway' import { generateText } from 'ai' import { getRedisConnection } from 'src/services/redisConnection' -import { keccak256, toHex } from 'viem' export const AI_MODEL = process.env.AI_MODEL || 'openai/gpt-4-turbo' const DEFAULT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 30 +const MAX_DAO_DESCRIPTION_LENGTH = 2200 const safeStringify = (value: unknown) => JSON.stringify(value, (_key, item) => @@ -16,8 +16,15 @@ export const getAiCacheKey = ( data: unknown, model: string = AI_MODEL ) => { - const hash = keccak256(toHex(`${safeStringify(data)}:${model}`)) - return `${namespace}:${hash}` + const payload = `${namespace}:${model}:${safeStringify(data)}` + let hash = 2166136261 + + for (let i = 0; i < payload.length; i += 1) { + hash ^= payload.charCodeAt(i) + hash = Math.imul(hash, 16777619) + } + + return `${namespace}:${(hash >>> 0).toString(16)}` } export const generateCachedAiText = async ({ @@ -36,7 +43,13 @@ export const generateCachedAiText = async ({ const redisConnection = getRedisConnection() const cacheKey = getAiCacheKey(namespace, data, model) - const cachedText = await redisConnection?.get(cacheKey) + if (!redisConnection) { + console.warn( + `[ai-cache] Redis unavailable for namespace=${namespace} key=${cacheKey}; skipping cache.` + ) + } + + const cachedText = redisConnection ? await redisConnection.get(cacheKey) : null if (cachedText) { return cachedText @@ -50,14 +63,19 @@ export const generateCachedAiText = async ({ const text = result.text.trim().replace(/\s+/g, ' ') - await redisConnection?.setex(cacheKey, ttlSeconds, text) + if (redisConnection) { + await redisConnection.setex(cacheKey, ttlSeconds, text) + } return text } -const trimDescription = (description: string, maxChars = 2200) => +const trimDescription = (description: string, maxChars = MAX_DAO_DESCRIPTION_LENGTH) => description.trim().replace(/\s+/g, ' ').slice(0, maxChars) +const sanitizeDescription = (description: string) => + description.replace(/###|assistant\s*:|system\s*:|user\s*:/gi, '') + const buildDaoDescriptionPrompt = ( description: string ) => `You are writing concise directory copy for a DAO platform. @@ -76,9 +94,22 @@ ${trimDescription(description)} Final Instruction: Respond with only the 1-2 sentence summary.` -export const summarizeDaoDescription = async (description: string) => - generateCachedAiText({ +export const summarizeDaoDescription = async (description: string) => { + if (description.length > MAX_DAO_DESCRIPTION_LENGTH) { + console.warn( + `[ai-cache] DAO description exceeded ${MAX_DAO_DESCRIPTION_LENGTH} chars; truncating before summarization.` + ) + } + + const cleaned = sanitizeDescription(trimDescription(description)) + + if (!cleaned.trim()) { + throw new Error('DAO description is empty after trimming/sanitization') + } + + return generateCachedAiText({ namespace: 'ai:daoDescription', - data: { description }, - prompt: buildDaoDescriptionPrompt(description), + data: { description: cleaned }, + prompt: buildDaoDescriptionPrompt(cleaned), }) +} diff --git a/package.json b/package.json index 1d89e9710..767376e31 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,5 @@ "test": "turbo run test", "type-check": "turbo run type-check", "version-packages": "changeset version" - }, - "dependencies": { - "remove-markdown": "^0.6.3" } } diff --git a/packages/sdk/src/subgraph/sdk.generated.ts b/packages/sdk/src/subgraph/sdk.generated.ts index 386ab26d5..dd4deb4df 100644 --- a/packages/sdk/src/subgraph/sdk.generated.ts +++ b/packages/sdk/src/subgraph/sdk.generated.ts @@ -30,6 +30,14 @@ export type Scalars = { Timestamp: { input: any; output: any } } +/** Indicates whether the current, partially filled bucket should be included in the response. Defaults to `exclude` */ +export enum Aggregation_Current { + /** Exclude the current, partially filled bucket from the response */ + Exclude = 'exclude', + /** Include the current, partially filled bucket in the response */ + Include = 'include', +} + export enum Aggregation_Interval { Day = 'day', Hour = 'hour', @@ -1283,10 +1291,8 @@ export type ClankerToken_Filter = { extensionsSupply_not?: InputMaybe extensionsSupply_not_in?: InputMaybe> extensions_contains?: InputMaybe> - extensions_contains_nocase?: InputMaybe> extensions_not?: InputMaybe> extensions_not_contains?: InputMaybe> - extensions_not_contains_nocase?: InputMaybe> holders_?: InputMaybe id?: InputMaybe id_gt?: InputMaybe @@ -4436,10 +4442,8 @@ export type Proposal_Filter = { snapshotBlockNumber_not_in?: InputMaybe> targets?: InputMaybe> targets_contains?: InputMaybe> - targets_contains_nocase?: InputMaybe> targets_not?: InputMaybe> targets_not_contains?: InputMaybe> - targets_not_contains_nocase?: InputMaybe> timeCreated?: InputMaybe timeCreated_gt?: InputMaybe timeCreated_gte?: InputMaybe @@ -4481,10 +4485,8 @@ export type Proposal_Filter = { updates_?: InputMaybe values?: InputMaybe> values_contains?: InputMaybe> - values_contains_nocase?: InputMaybe> values_not?: InputMaybe> values_not_contains?: InputMaybe> - values_not_contains_nocase?: InputMaybe> vetoTransactionHash?: InputMaybe vetoTransactionHash_contains?: InputMaybe vetoTransactionHash_gt?: InputMaybe diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7be9459f5..82a95c3cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,10 +18,6 @@ overrides: importers: .: - dependencies: - remove-markdown: - specifier: ^0.6.3 - version: 0.6.3 devDependencies: '@buildeross/eslint-config-custom': specifier: workspace:* @@ -173,7 +169,7 @@ importers: version: 2.2.10(@tanstack/react-query@5.90.3(react@19.2.1))(@types/react@19.2.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(viem@2.47.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(wagmi@2.18.1(@tanstack/query-core@5.90.3)(@tanstack/react-query@5.90.3(react@19.2.1))(@types/react@19.2.2)(bufferutil@4.0.9)(encoding@0.1.13)(ioredis@5.8.1)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))(zod@4.1.12)) '@sentry/nextjs': specifier: ^9.22.0 - version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) + version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) '@smartinvoicexyz/types': specifier: ^0.1.27 version: 0.1.28(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.1))(@types/react@19.2.2)(react@19.2.1))(@types/node@22.18.10)(@types/react@19.2.2)(bufferutil@4.0.9)(encoding@0.1.13)(framer-motion@12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(graphql-ws@6.0.6(crossws@0.3.5)(graphql@16.11.0)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(graphql@16.11.0)(react-dom@19.2.1(react@19.2.1))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) @@ -1349,7 +1345,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^9.22.0 - version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) + version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1) graphql: specifier: ^16.11.0 version: 16.11.0 @@ -16521,7 +16517,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1)': + '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.9(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.102.1)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.37.0