Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/comparison/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MarketingPage } from '@/components/MarketingPage'

const Layout = ({ children }) => <MarketingPage>{children}</MarketingPage>
const Layout = ({ children }) => <MarketingPage withGradient>{children}</MarketingPage>
export default Layout
2 changes: 1 addition & 1 deletion src/app/migrate/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MarketingPage } from '@/components/MarketingPage'

const Layout = ({ children }) => <MarketingPage>{children}</MarketingPage>
const Layout = ({ children }) => <MarketingPage withGradient>{children}</MarketingPage>
export default Layout
44 changes: 7 additions & 37 deletions src/app/showcase/page.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
'use client'

import { useEffect, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { formatNumber } from '@/lib/common'
import { ShowcaseGradient } from '@/components/ShowcaseGradient'
import companies from './companies.json'
import testimonials from '../../../utils/testimonials.json'

Expand All @@ -30,44 +27,17 @@ const logoMap = {
}

export default function ShowcasePage() {
// Display the cached count if available; otherwise leave empty rather
// than show a stale fallback ("3500+" was years out of date).
const [githubStars, setGithubStars] = useState(null)

useEffect(() => {
let cached = localStorage.getItem('githubStars')
if (cached) {
setGithubStars(formatNumber(Number(cached)))
return
}
// No cached value — fetch live so first-time visitors see a number.
fetch('https://api.github.com/repos/gofr-dev/gofr')
.then((r) => (r.ok ? r.json() : null))
.then((d) => {
if (d?.watchers) {
setGithubStars(formatNumber(d.watchers))
try { localStorage.setItem('githubStars', String(d.watchers)) } catch {}
}
})
.catch(() => {})
}, [])

return (
<div className="min-h-screen bg-slate-900">
<div className="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
{/* Hero */}
<div className="relative min-h-screen overflow-x-clip bg-slate-900">
<ShowcaseGradient />
<div className="relative mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
{/* Hero — star count is already shown live in the header,
so we don't repeat it here (the cached client-side number
could lag the header by hours). */}
<div className="text-center">
<h1 className="font-display text-4xl font-bold tracking-tight text-white">
Used by engineers around the world
</h1>
{githubStars && (
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-slate-800 px-4 py-2 text-sm text-slate-300">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#fcd34d" className="h-4 w-4">
<path fillRule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z" clipRule="evenodd" />
</svg>
{githubStars} stars on GitHub
</div>
)}
</div>

{/* Company Grid */}
Expand Down
2 changes: 1 addition & 1 deletion src/app/why-gofr/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MarketingPage } from '@/components/MarketingPage'

const Layout = ({ children }) => <MarketingPage>{children}</MarketingPage>
const Layout = ({ children }) => <MarketingPage withGradient>{children}</MarketingPage>
export default Layout
19 changes: 15 additions & 4 deletions src/components/AnimatedTimelineItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,28 @@ import React, { useRef, useEffect, useState } from 'react';

const AnimatedTimelineItem = ({ date, title, description, imageSrc, isLeft }) => {
const itemRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
// Default `isVisible: true` so the image renders fully visible
// for users with `prefers-reduced-motion: reduce`, for SSR/no-JS,
// and for any environment where the IntersectionObserver doesn't
// fire (e.g. Playwright fullPage screenshot capture). Without
// this, the image stayed at opacity 0 and looked like the
// timeline was missing half its photos.
const [isVisible, setIsVisible] = useState(true);
const [isExiting, setIsExiting] = useState(false);

useEffect(() => {
if (typeof window === 'undefined') return;
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
if (reduce) return; // already visible=true, skip the observer-driven fade
// Start hidden post-hydration so the entry animation can play.
setIsVisible(false);
const observer = new IntersectionObserver(
Comment on lines +11 to 20
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
setIsExiting(false);
setIsVisible(true);
setIsExiting(false);
} else {
setIsVisible(false);
setIsVisible(false);
setIsExiting(true);
}
},
Expand Down
8 changes: 6 additions & 2 deletions src/components/CompanyTrustedList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ export default function CompanyList() {
/>{' '}
Developers at Companies Like:
</h2>
<div className="mt-10 flex flex-wrap justify-center gap-x-0 gap-y-10 sm:max-w-none lg:mx-0">
{/* 9 logos: pick column counts that divide evenly so we
don't end on a ragged orphan row. 3 cols at every
breakpoint (3+3+3) is more deliberate than the previous
4+4+1 (mobile), 6+3 (sm), 5+4 (lg) ladder. */}
<div className="mt-10 grid grid-cols-3 justify-items-center gap-x-6 gap-y-10">
{imageLink.map((item, index) => (
<div
key={index}
className="flex max-h-12 w-1/4 justify-center sm:w-1/6 lg:w-1/5"
className="flex max-h-12 w-full justify-center"
>
<Image
alt={item.name}
Expand Down
4 changes: 3 additions & 1 deletion src/components/HomePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EcosystemRecognition } from '@/components/EcosystemRecognition'
import { FadeInOnScroll } from '@/components/FadeInOnScroll'
import { StaggerChildren } from '@/components/StaggerChildren'
import blurCyanImage from "@/images/blur-cyan.png";
import { ShowcaseGradient } from '@/components/ShowcaseGradient'
import Image from "next/image";
import React from "react";
import Testimonials from "@/components/Reviews";
Expand All @@ -19,7 +20,8 @@ import Testimonials from "@/components/Reviews";
// continuous strip rather than two seams.
export function HomePage() {
return (
<div className="m-auto w-auto max-w-screen-2xl">
<div className="relative m-auto w-auto max-w-screen-2xl overflow-x-clip">
<ShowcaseGradient />
{/* Density-matched section padding. Uniform `py-24` everywhere
made the page feel padded-for-padding's-sake when section
content was thin (a 100 px marquee strip, a 1-row logo wall).
Expand Down
17 changes: 15 additions & 2 deletions src/components/MarketingPage.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { ShowcaseGradient } from '@/components/ShowcaseGradient'

// Wrapper for the SEO-only Markdoc pages (/why-gofr, /comparison/*,
// /migrate/*, /learn, /faq). It supplies the flex container DocsLayout
// expects, but unlike DocsPage it does NOT render a left sidebar — these
// pages aren't core navigation, they're SEO landing content. Right-rail
// "On this page" TOC from DocsLayout still renders. Cross-links to the
// other SEO pages live in the global footer.
export function MarketingPage({ children }) {
//
// `withGradient` adds the decorative orb layer (ShowcaseGradient).
// Opt-in because /faq and /learn are reading-mode reference pages
// where the gradient is just visual noise; /why-gofr, /comparison/*,
// /migrate/* are positioning surfaces where it belongs.
export function MarketingPage({ children, withGradient = false }) {
return (
<div>
<div className="relative mx-auto flex w-full max-w-screen-2xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
<div
className={
'relative mx-auto flex w-full max-w-screen-2xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12' +
(withGradient ? ' overflow-x-clip' : '')
}
>
{withGradient && <ShowcaseGradient />}
{children}
</div>
</div>
Expand Down
21 changes: 17 additions & 4 deletions src/components/Prose.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,26 @@ export function Prose({ as, className, ...props }) {
'prose-a:font-semibold dark:prose-a:text-sky-400',
// link underline
'prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,theme(colors.sky.300))] hover:prose-a:[--tw-prose-underline-size:6px] dark:[--tw-prose-background:theme(colors.slate.900)] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,theme(colors.sky.800))] dark:hover:prose-a:[--tw-prose-underline-size:6px]',
// pre
'prose-pre:rounded-lg prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:shadow-none dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10',
// inline code
'prose-code:rounded prose-code:bg-slate-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[0.8125em] prose-code:font-medium prose-code:before:content-none prose-code:after:content-none dark:prose-code:bg-slate-800 dark:prose-code:text-slate-300',
// pre — `overflow-x-auto` lets long lines scroll inside the
// block instead of widening the prose column (the latter
// forced page-level horizontal scroll on docs/configs etc.
// at narrow viewports).
'prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:shadow-none dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10',
// inline code — `break-words` so long unbreakable tokens
// (env-var names, connection strings, fully-qualified Go
// identifiers) wrap instead of pushing the column wider
// than the viewport.
'prose-code:rounded prose-code:bg-slate-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[0.8125em] prose-code:font-medium prose-code:break-words prose-code:before:content-none prose-code:after:content-none dark:prose-code:bg-slate-800 dark:prose-code:text-slate-300',
// hr
'dark:prose-hr:border-slate-800',
// tables
// Intentionally NOT applying `display: block` + horizontal
// scroll here. For reference pages like /docs/references/configs
// the page-level overflow at 375px is actually more useful than
// a per-table scroll: every column stays visible at once (small
// but scannable), versus hiding 2 of 4 columns behind a thin,
// discovery-hostile scrollbar. The trade-off favors information
// density on these reference surfaces.
'prose-table:text-sm',
'prose-thead:border-b prose-thead:border-slate-200 dark:prose-thead:border-slate-700',
'prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:text-slate-900 dark:prose-th:text-slate-200',
Expand Down
8 changes: 6 additions & 2 deletions src/components/Reviews.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ export default function Testimonials() {
{/*<h2 className="text-center text-lg font-semibold leading-8 text-white mb-10">*/}
{/* User Testimonials*/}
{/*</h2>*/}
<div className="sm:columns-2 md:columns-3">
<div className="gap-x-6 sm:columns-2 md:columns-3">
{testimonials.map((testimonial, index) => (
<div key={index} className="group inline-block relative my-3 rounded-xl border cursor-default border-slate-200 dark:border-slate-800 p-1">
// `block break-inside-avoid` keeps each card whole inside
// the CSS multi-column flow. Without it, a tall card gets
// split at the column boundary and the two halves render
// on top of each other on narrow viewports.
<div key={index} className="group block break-inside-avoid w-full relative my-3 rounded-xl border cursor-default border-slate-200 dark:border-slate-800 p-1">
<div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,theme(colors.sky.50)),var(--quick-links-hover-bg,theme(colors.sky.50)))_padding-box,linear-gradient(to_top,theme(colors.indigo.400),theme(colors.cyan.400),theme(colors.sky.500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:theme(colors.slate.800)]" />

<div className="relative overflow-hidden rounded-xl p-6">
Expand Down
42 changes: 31 additions & 11 deletions src/components/Search.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,27 +165,47 @@ function HighlightQuery({ text, query }) {
)
}

// Known-token map for breadcrumb/segment prettifying. Naive Title
// Case turned `gofr-vs-gin` into `Gofr Vs Gin` — wrong on both
// counts (brand mark is `GoFr`, comparator is lowercase `vs`) and
// `wrap-grpc` into `Wrap Grpc` instead of `Wrap gRPC`. Anything
// not in this map falls back to plain Title Case.
const PRETTY_TOKENS = {
gofr: 'GoFr',
gofrcli: 'GoFr CLI',
vs: 'vs',
grpc: 'gRPC',
graphql: 'GraphQL',
api: 'API',
cli: 'CLI',
http: 'HTTP',
https: 'HTTPS',
rest: 'REST',
faq: 'FAQ',
}

// Turn a path segment slug into a Title Case label.
// "wrap-grpc" → "Wrap Grpc", "from-fiber" → "From Fiber".
// Strict slug-to-words; preserves embedded acronyms only when they're already uppercase.
// "wrap-grpc" → "Wrap gRPC", "from-fiber" → "From Fiber",
// "gofr-vs-gin" → "GoFr vs Gin".
function prettifySegment(seg) {
if (!seg) return ''
return seg
.split(/[-_]/)
.map((w) =>
w.length === 0
? ''
: w === w.toUpperCase()
? w
: w.charAt(0).toUpperCase() + w.slice(1),
)
.map((w) => {
if (w.length === 0) return ''
const known = PRETTY_TOKENS[w.toLowerCase()]
if (known) return known
// Already uppercase — preserve embedded acronyms.
if (w === w.toUpperCase()) return w
return w.charAt(0).toUpperCase() + w.slice(1)
})
.join(' ')
}

// Build a breadcrumb-shaped path label from a URL.
// "/docs/datasources/mongodb" → "Datasources › Mongodb"
// "/docs/references/gofrcli/init" → "References › Gofrcli › Init"
// "/comparison/gofr-vs-gin" → "Comparison › Gofr Vs Gin"
// "/docs/references/gofrcli/init" → "References › GoFr CLI › Init"
// "/comparison/gofr-vs-gin" → "Comparison › GoFr vs Gin"
function pathBreadcrumb(rawUrl) {
if (!rawUrl) return ''
let p = rawUrl.split('#')[0].split('?')[0]
Expand Down
56 changes: 56 additions & 0 deletions src/components/ShowcaseGradient.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Image from 'next/image'
import blurCyanImage from '@/images/blur-cyan.png'
import blurIndigoImage from '@/images/blur-indigo.png'

// Decorative gradient layer for the framework's pitch / showcase
// surfaces — homepage, /why-gofr, /comparison/*, /showcase, /migrate/*.
// Three pointer-events-none orbs distributed down the page at
// alternating sides, reusing the cyan + indigo vocabulary the hero
// already establishes. Drop inside any `relative overflow-x-clip`
// wrapper.
//
// Why an absolute layer instead of a CSS background-image:
// - Matches the existing Hero implementation (Image PNGs, opacity).
// - Lets us anchor each orb relative to a known parent, so different
// pages can opt in without re-tuning a body-level gradient.
//
// Why `unoptimized` is mandatory: the site builds with
// `output: 'export'`, which disables /_next/image. Without unoptimized,
// Next emits a /_next/image?url=... src that 404s in production and
// renders as a broken-image icon.
export function ShowcaseGradient() {
return (
<>
<Image
aria-hidden="true"
className="pointer-events-none absolute -left-32 top-[30%] hidden select-none opacity-20 sm:block"
src={blurCyanImage}
alt=""
width={530}
height={530}
loading="lazy"
unoptimized
/>
<Image
aria-hidden="true"
className="pointer-events-none absolute -right-40 top-[55%] hidden select-none opacity-20 sm:block"
src={blurIndigoImage}
alt=""
width={567}
height={567}
loading="lazy"
unoptimized
/>
<Image
aria-hidden="true"
className="pointer-events-none absolute -left-40 top-[80%] hidden select-none opacity-15 sm:block"
src={blurCyanImage}
alt=""
width={530}
height={530}
loading="lazy"
unoptimized
/>
</>
)
}
Loading
Loading