mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-27 04:01:12 +00:00
fix: update responsive styles and improve layout across various components
This commit is contained in:
@@ -60,3 +60,7 @@ apps/*/public/fonts/**/*.otf
|
||||
|
||||
mcp.json
|
||||
.env*.local
|
||||
|
||||
# Playwright / browser-MCP artifacts (never commit)
|
||||
.playwright-mcp/
|
||||
**/.playwright-mcp/
|
||||
|
||||
@@ -6,22 +6,22 @@ import type { LambdaPrizePageCopy } from './types'
|
||||
|
||||
export function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
|
||||
return (
|
||||
<section className="h-[406px] bg-brand-off-white px-0 py-10 text-brand-dark-green min-[1025px]:h-[421px] min-[1025px]:px-3 min-[1025px]:py-12">
|
||||
<section className="h-[406px] bg-brand-off-white px-0 py-10 text-brand-dark-green lg:h-[421px] lg:px-3 lg:py-12">
|
||||
<ContentWidth>
|
||||
<div className="grid grid-cols-[1fr_auto] gap-y-6 min-[1025px]:grid-cols-3">
|
||||
<div className="grid grid-cols-[1fr_auto] gap-y-6 lg:grid-cols-3">
|
||||
<h2 className="text-h3-serif">{copy.heading}</h2>
|
||||
<p className="text-mono-s col-start-1 row-start-2 w-[178px] min-[1025px]:col-start-auto min-[1025px]:row-start-auto min-[1025px]:w-[226px]">
|
||||
<p className="text-mono-s col-start-1 row-start-2 w-[178px] lg:col-start-auto lg:row-start-auto lg:w-[226px]">
|
||||
{copy.body}
|
||||
</p>
|
||||
<div className="col-start-2 row-start-1 min-[1025px]:col-start-auto min-[1025px]:row-start-auto">
|
||||
<div className="col-start-2 row-start-1 lg:col-start-auto lg:row-start-auto">
|
||||
<TertiaryCta href={ROUTES.faq}>{copy.cta}</TertiaryCta>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 min-[1025px]:mt-8">
|
||||
<div className="mt-6 lg:mt-8">
|
||||
{copy.rows.map((row, index) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className={`grid h-[58px] grid-cols-[1fr_83px] items-start px-3 py-3 min-[1025px]:h-[50px] min-[1025px]:grid-cols-[714fr_464fr_238fr] ${
|
||||
className={`grid h-[58px] grid-cols-[1fr_83px] items-start px-3 py-3 lg:h-[50px] lg:grid-cols-[714fr_464fr_238fr] ${
|
||||
index % 2 === 0 ? 'bg-gray-01' : 'bg-brand-dark-green/5'
|
||||
}`}
|
||||
>
|
||||
@@ -31,7 +31,7 @@ export function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
|
||||
</p>
|
||||
<h3 className="text-body-serif">{row.label}</h3>
|
||||
</div>
|
||||
<p className="text-mono-s hidden max-w-[312px] min-[1025px]:block">
|
||||
<p className="text-mono-s hidden max-w-[312px] lg:block">
|
||||
{row.body}
|
||||
</p>
|
||||
<div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function RfpListRow({ index, rfp }: Props) {
|
||||
>
|
||||
{/* Mobile / tablet (below 1025px): 2-column flow grid so a long title
|
||||
pushes the description down instead of overlapping it. */}
|
||||
<div className="grid min-h-[116px] grid-cols-[1fr_auto] items-start gap-x-3 gap-y-3 p-3 min-[1025px]:hidden">
|
||||
<div className="grid min-h-[116px] grid-cols-[1fr_auto] items-start gap-x-3 gap-y-3 p-3 lg:hidden">
|
||||
<p className="font-sans text-[14px] leading-[1.2] text-brand-dark-green">
|
||||
<span className="font-medium">{indexLabel}</span>
|
||||
<span className="ml-3 font-display">{rfp.title}</span>
|
||||
@@ -56,7 +56,7 @@ export function RfpListRow({ index, rfp }: Props) {
|
||||
{/* Desktop (1025px+): single flex row. The reward + CTA block is
|
||||
shrink-0 so it always stays inside the table; title and description
|
||||
truncate as the window narrows. */}
|
||||
<div className="hidden h-[50px] items-start gap-3 px-3 pt-3 min-[1025px]:flex">
|
||||
<div className="hidden h-[50px] items-start gap-3 px-3 pt-3 lg:flex">
|
||||
<div className="flex w-1/2 min-w-0 items-baseline gap-3">
|
||||
<span className="w-[18px] shrink-0 font-sans text-[14px] font-normal leading-[1.2] text-brand-dark-green">
|
||||
{indexLabel}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function ContributeSection({
|
||||
jobs: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green md:min-h-[262px]">
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white py-10 text-brand-dark-green md:min-h-[262px]">
|
||||
<ContentWidth className="grid gap-10 md:grid-cols-12 md:gap-3">
|
||||
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] md:col-span-3">
|
||||
{title}
|
||||
|
||||
@@ -15,17 +15,17 @@ export function OverviewSection({
|
||||
cta: LinkItem
|
||||
}) {
|
||||
return (
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green min-[1025px]:min-h-[670px]">
|
||||
<ContentWidth className="grid gap-10 min-[1025px]:grid-cols-12 min-[1025px]:gap-3">
|
||||
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] min-[1025px]:col-span-3">
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white py-10 text-brand-dark-green lg:min-h-[670px]">
|
||||
<ContentWidth className="grid gap-10 lg:grid-cols-12 lg:gap-3">
|
||||
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] lg:col-span-3">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="text-mono-s space-y-[13px] min-[1025px]:col-span-3 min-[1025px]:col-start-7 min-[1025px]:max-w-[345px]">
|
||||
<div className="text-mono-s space-y-[13px] lg:col-span-3 lg:col-start-7 lg:max-w-[345px]">
|
||||
{paragraphs.map((paragraph, index) => (
|
||||
<p key={index}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="min-[1025px]:col-span-2 min-[1025px]:col-start-11 min-[1025px]:justify-self-start">
|
||||
<div className="lg:col-span-2 lg:col-start-11 lg:justify-self-start">
|
||||
<LinkButton {...cta} />
|
||||
</div>
|
||||
</ContentWidth>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function ArticleCard({
|
||||
return (
|
||||
<ExternalLink
|
||||
href={href}
|
||||
className="group flex w-84.75 shrink-0 cursor-pointer flex-col gap-1.5 min-[1025px]:w-auto"
|
||||
className="group flex w-84.75 shrink-0 cursor-pointer flex-col gap-1.5 lg:w-auto"
|
||||
>
|
||||
<div className="relative aspect-339/431 w-full overflow-hidden">
|
||||
<Image
|
||||
@@ -76,8 +76,8 @@ export function ArticleCard({
|
||||
<p className={titleClassName}>
|
||||
{mobileTitle ? (
|
||||
<>
|
||||
<span className="min-[1025px]:hidden">{mobileTitle}</span>
|
||||
<span className="hidden min-[1025px]:inline">{title}</span>
|
||||
<span className="lg:hidden">{mobileTitle}</span>
|
||||
<span className="hidden lg:inline">{title}</span>
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getTechStackRelatedArticleCards } from '@/lib/tech-stack-related-articl
|
||||
import { ArticleCard } from './related-articles-card'
|
||||
|
||||
const TITLE_CLASSNAME =
|
||||
'text-caption-sans w-[169.5px] shrink-0 font-medium text-brand-dark-green min-[1025px]:text-[14px] min-[1025px]:leading-[1.2]'
|
||||
'text-caption-sans w-[169.5px] shrink-0 font-medium text-brand-dark-green lg:text-[14px] lg:leading-[1.2]'
|
||||
|
||||
type Props = {
|
||||
data: RelatedArticlesSection
|
||||
@@ -36,51 +36,51 @@ export default function TechStackRelatedArticles({
|
||||
className={twMerge('h-220 px-3 pb-3 pt-0', contentClassName)}
|
||||
>
|
||||
<Reveal
|
||||
amount={0.15}
|
||||
className="relative h-full overflow-hidden rounded-xl bg-accent-tan px-3 pt-6 pb-10 min-[1025px]:px-0 min-[1025px]:pt-0 min-[1025px]:pb-0"
|
||||
>
|
||||
<div className="flex items-start justify-between min-[1025px]:block">
|
||||
{data.label ? (
|
||||
<p className="text-mono-s w-56.5 max-w-[50%] text-brand-dark-green min-[1025px]:absolute min-[1025px]:top-6 min-[1025px]:left-3 min-[1025px]:max-w-none">
|
||||
{data.mobileLabel ?? data.label}
|
||||
</p>
|
||||
) : null}
|
||||
{data.eyebrow ? (
|
||||
// xl, not min-[1025px]: the fixed left-178.5 offset collides with the
|
||||
// right-anchored CTA on viewports narrower than ~1190px.
|
||||
<p className="text-mono-s absolute top-6 left-178.5 hidden w-56.5 text-brand-dark-green xl:block">
|
||||
{data.eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
{data.cta ? (
|
||||
<div className="flex w-56.5 max-w-[50%] justify-end min-[1025px]:absolute min-[1025px]:top-5.5 min-[1025px]:right-[87px] min-[1025px]:block min-[1025px]:w-auto min-[1025px]:max-w-none">
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="tertiary"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<h2 className="mt-16.25 text-center font-display text-[40px] leading-none tracking-[-0.03em] whitespace-nowrap text-brand-dark-green min-[1025px]:absolute min-[1025px]:top-25.5 min-[1025px]:left-1/2 min-[1025px]:mt-0 min-[1025px]:w-116 min-[1025px]:-translate-x-1/2 min-[1025px]:text-h3-serif">
|
||||
{data.title}
|
||||
</h2>
|
||||
|
||||
<Reveal
|
||||
stagger
|
||||
amount={0.2}
|
||||
className="mt-25 flex gap-3 overflow-x-auto min-[1025px]:absolute min-[1025px]:top-60.25 min-[1025px]:right-3 min-[1025px]:left-3 min-[1025px]:mt-0 min-[1025px]:grid min-[1025px]:grid-cols-4 min-[1025px]:overflow-visible"
|
||||
amount={0.15}
|
||||
className="relative h-full overflow-hidden rounded-xl bg-accent-tan px-3 pt-6 pb-10 lg:px-0 lg:pt-0 lg:pb-0"
|
||||
>
|
||||
{cards.map((card) => (
|
||||
<RevealItem key={`${card.href}:${card.title}`}>
|
||||
<ArticleCard {...card} titleClassName={TITLE_CLASSNAME} />
|
||||
</RevealItem>
|
||||
))}
|
||||
<div className="flex items-start justify-between lg:block">
|
||||
{data.label ? (
|
||||
<p className="text-mono-s w-56.5 max-w-[50%] text-brand-dark-green lg:absolute lg:top-6 lg:left-3 lg:max-w-none">
|
||||
{data.mobileLabel ?? data.label}
|
||||
</p>
|
||||
) : null}
|
||||
{data.eyebrow ? (
|
||||
// xl, not lg: the fixed left-178.5 offset collides with the
|
||||
// right-anchored CTA on viewports narrower than ~1190px.
|
||||
<p className="text-mono-s absolute top-6 left-178.5 hidden w-56.5 text-brand-dark-green xl:block">
|
||||
{data.eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
{data.cta ? (
|
||||
<div className="flex w-56.5 max-w-[50%] justify-end lg:absolute lg:top-5.5 lg:right-[87px] lg:block lg:w-auto lg:max-w-none">
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="tertiary"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<h2 className="mt-16.25 text-center font-display text-[40px] leading-none tracking-[-0.03em] whitespace-nowrap text-brand-dark-green lg:absolute lg:top-25.5 lg:left-1/2 lg:mt-0 lg:w-116 lg:-translate-x-1/2 lg:text-h3-serif">
|
||||
{data.title}
|
||||
</h2>
|
||||
|
||||
<Reveal
|
||||
stagger
|
||||
amount={0.2}
|
||||
className="mt-25 flex gap-3 overflow-x-auto lg:absolute lg:top-60.25 lg:right-3 lg:left-3 lg:mt-0 lg:grid lg:grid-cols-4 lg:overflow-visible"
|
||||
>
|
||||
{cards.map((card) => (
|
||||
<RevealItem key={`${card.href}:${card.title}`}>
|
||||
<ArticleCard {...card} titleClassName={TITLE_CLASSNAME} />
|
||||
</RevealItem>
|
||||
))}
|
||||
</Reveal>
|
||||
</Reveal>
|
||||
</Reveal>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { rewriteRfpMarkdownLinks } from '@/lib/rfps-github'
|
||||
|
||||
const FILE_URL =
|
||||
'https://github.com/logos-co/rfp/blob/master/RFPs/RFP-016-lbp-launchpad.md'
|
||||
|
||||
describe('rewriteRfpMarkdownLinks', () => {
|
||||
it('rewrites sibling RFP cross-references to internal detail routes', () => {
|
||||
const md = 'See [LBP](./RFP-008-lending-borrowing-protocol.md) for context.'
|
||||
|
||||
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(
|
||||
'See [LBP](/builders-hub/rfps/lending-borrowing-protocol) for context.'
|
||||
)
|
||||
})
|
||||
|
||||
it('rewrites RFP references without a leading ./', () => {
|
||||
const md = '[admin](RFP-001-admin-authority-lib.md)'
|
||||
|
||||
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(
|
||||
'[admin](/builders-hub/rfps/admin-authority-lib)'
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves other repo-relative .md links to absolute GitHub URLs, keeping anchors', () => {
|
||||
const md =
|
||||
'[fees](../appendix/token-launchpad-ecosystem.md#fee-structures)'
|
||||
|
||||
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(
|
||||
'[fees](https://github.com/logos-co/rfp/blob/master/appendix/token-launchpad-ecosystem.md#fee-structures)'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves external, absolute, and anchor links unchanged', () => {
|
||||
const md = [
|
||||
'[ext](https://github.com/logos-co/lambda-prize/blob/master/prizes/LP-0004.md)',
|
||||
'[abs](/builders-hub/rfps)',
|
||||
'[frag](#overview)',
|
||||
].join('\n')
|
||||
|
||||
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(md)
|
||||
})
|
||||
|
||||
it('leaves relative non-markdown links untouched', () => {
|
||||
const md = ''
|
||||
|
||||
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(md)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import matter from 'gray-matter'
|
||||
import { cache } from 'react'
|
||||
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import type { RfpListItem } from '@/lib/rfp-types'
|
||||
|
||||
/**
|
||||
@@ -73,6 +74,50 @@ const toSlug = (filename: string, fallback: string): string => {
|
||||
return slug || fallback.toLowerCase()
|
||||
}
|
||||
|
||||
/** Matches the URL target of a markdown inline link/image: `](target)`. */
|
||||
const MARKDOWN_LINK_TARGET = /\]\(\s*(<[^>]+>|[^)\s]+)\s*\)/g
|
||||
|
||||
const isAbsoluteOrAnchor = (href: string): boolean =>
|
||||
/^(https?:\/\/|\/|#|mailto:)/i.test(href)
|
||||
|
||||
/**
|
||||
* Rewrites repo-relative markdown links so RFP detail pages don't 404. Upstream
|
||||
* RFP files cross-reference each other with paths relative to the repo's
|
||||
* `RFPs/` directory (e.g. `./RFP-008-lending-borrowing-protocol.md`), which the
|
||||
* browser would otherwise resolve against the page URL and 404.
|
||||
*
|
||||
* - `RFP-NNN-*.md` references become the internal detail route
|
||||
* (`/builders-hub/rfps/<slug>`), using the same slug the pages are built with.
|
||||
* - Other repo-relative `.md` links (e.g. `../appendix/…`) have no site page, so
|
||||
* they resolve to their absolute GitHub URL (anchors preserved).
|
||||
* - Absolute, external, and anchor links are left untouched.
|
||||
*
|
||||
* `fileHtmlUrl` is the GitHub `blob` URL of the file being parsed; it anchors
|
||||
* the resolution of the remaining relative links.
|
||||
*/
|
||||
export const rewriteRfpMarkdownLinks = (
|
||||
markdown: string,
|
||||
fileHtmlUrl: string
|
||||
): string =>
|
||||
markdown.replace(MARKDOWN_LINK_TARGET, (match, rawTarget: string) => {
|
||||
const href = rawTarget.replace(/^<|>$/g, '')
|
||||
if (isAbsoluteOrAnchor(href)) return match
|
||||
|
||||
const [path] = href.split('#')
|
||||
if (!/\.md$/i.test(path)) return match
|
||||
|
||||
const filename = path.split('/').pop() ?? ''
|
||||
if (/^RFP-\d+/i.test(filename)) {
|
||||
return `](${ROUTES.rfps}/${toSlug(filename, filename)})`
|
||||
}
|
||||
|
||||
try {
|
||||
return `](${new URL(href, fileHtmlUrl).href})`
|
||||
} catch {
|
||||
return match
|
||||
}
|
||||
})
|
||||
|
||||
const firstMatch = (raw: string, patterns: RegExp[]): string | undefined => {
|
||||
for (const pattern of patterns) {
|
||||
const value = raw.match(pattern)?.[1]?.replace(/[`*]/g, '').trim()
|
||||
@@ -169,7 +214,7 @@ const parseRfpMarkdown = (
|
||||
status,
|
||||
tier: tier ?? '',
|
||||
githubUrl: htmlUrl,
|
||||
rawMarkdown: content.trim(),
|
||||
rawMarkdown: rewriteRfpMarkdownLinks(content.trim(), htmlUrl),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user