Skip to content
Merged
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 thread
Umang01-hash marked this conversation as resolved.
([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
Comment thread
Umang01-hash marked this conversation as resolved.
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 (
Comment thread
Umang01-hash marked this conversation as resolved.
<>
<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