Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
a5e9e3f
Extract both LookingForMore instances to one component
hasparus Dec 5, 2025
6acec96
Add `ResourceTag` type
hasparus Dec 5, 2025
44b9aed
Add JSON files for Resources Hub data
hasparus Dec 5, 2025
87c212b
Add Resources missing from Northstar's Google Sheets
hasparus Dec 5, 2025
aafdec9
Add arktype to main package
hasparus Dec 5, 2025
54bb9c1
Add icons from Figma
hasparus Dec 5, 2025
a7465ab
Add ResourceMetadata schema
hasparus Dec 5, 2025
9d2d29b
Make LearnHeroStripes configurable
hasparus Dec 5, 2025
e7d8bbd
Make the blog section mobile friendly
hasparus Dec 5, 2025
99ab5a9
Add categories section
hasparus Dec 5, 2025
347046c
Add resources hub index
hasparus Dec 5, 2025
d0620a0
Add reading resources section
hasparus Dec 5, 2025
295bf58
Add Resources Hero
hasparus Dec 5, 2025
04cc435
Add specification section
hasparus Dec 5, 2025
ecc2e08
Add tools and libraries section
hasparus Dec 5, 2025
c16db1b
Add video resources section
hasparus Dec 5, 2025
a9f5b56
Add redirects from old to new pages
hasparus Dec 5, 2025
8ba9c40
Lock arktype version in sync-working-groups package
hasparus Dec 5, 2025
3bd6193
Add transition
hasparus Dec 5, 2025
9a52ec1
Tweak colors
hasparus Dec 5, 2025
91f0375
Use proper icon
hasparus Dec 5, 2025
888aea5
Optimize and simplify icons
hasparus Dec 5, 2025
ae2a582
Format
hasparus Dec 5, 2025
b9efe84
Make the Specification section pink
hasparus Dec 6, 2025
2e1c8b1
Use the Eyebrow component
hasparus Dec 6, 2025
debbeda
Update styles
hasparus Dec 6, 2025
5f8b974
Tag `src/code/*.mdx` files
hasparus Dec 6, 2025
0c55c11
Read json files
hasparus Dec 6, 2025
a578a02
Import proper icons
hasparus Dec 6, 2025
168b712
Use proper icons, tweak layout
hasparus Dec 6, 2025
cc0d8da
Improve styles
hasparus Dec 6, 2025
b7c2aad
Improve the styles
hasparus Dec 8, 2025
faa585d
Truncate breadcrumbs
hasparus Dec 8, 2025
49f128b
Render blog category links in blog section
hasparus Dec 8, 2025
f7a8eca
Improve styles
hasparus Dec 8, 2025
c9eaa31
Implement mobile styles for blog section
hasparus Dec 8, 2025
640a164
Improve category list style
hasparus Dec 8, 2025
1b03575
Unify card styling with the new design
hasparus Dec 8, 2025
fb00a16
Draft `resources/[category]` page.
hasparus Dec 8, 2025
874ebc0
Change "client" tag to "frontend"
hasparus Dec 8, 2025
df9eb6a
Read code/**/*.mdx
hasparus Dec 9, 2025
8be74e4
change server tag to backend tag
hasparus Dec 9, 2025
f811d67
Improve styles
hasparus Dec 9, 2025
ae3397e
Add typography-h4
hasparus Dec 9, 2025
d44f4d4
Link to proper subpages
hasparus Dec 9, 2025
bc5c685
Add ResourceHubCard
hasparus Dec 9, 2025
abb04fe
Add missing authors
hasparus Dec 9, 2025
16c1457
wip
hasparus Dec 9, 2025
c92c199
Read blog articles, remove redundant JSON files
hasparus Dec 9, 2025
cdb6b50
Remove duplicates
hasparus Dec 9, 2025
b245d89
Change tag to blog-or-newsletter to disambiguate with "blog or post"
hasparus Dec 9, 2025
bf468e2
Improve styles
hasparus Dec 9, 2025
824158c
Add left borders to second column
hasparus Dec 9, 2025
1fb8512
Add icons
hasparus Dec 10, 2025
0f00797
Render an icon and chevron-down
hasparus Dec 10, 2025
a198b9b
Run format
hasparus Dec 10, 2025
4df4540
Use currentColor in icons and remove redundant clipPaths
hasparus Dec 11, 2025
55d9258
Use `canvas` color
hasparus Dec 11, 2025
08415d8
Bump deps
hasparus Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
"@tailwindcss/typography": "^0.5.15",
"arktype": "2.1.28",
"autoprefixer": "^10.4.20",
"calendar-link": "^2.10.0",
"clsx": "^2.1.1",
Expand All @@ -59,7 +60,7 @@
"leaflet": "^1.9.4",
"lucide-react": "^0.469.0",
"motion": "^12.11.0",
"next": "^14.2.32",
"next": "14.2.34",
"next-query-params": "^5.0.1",
"next-sitemap": "^4.2.3",
"next-with-less": "^3.0.1",
Expand Down
104 changes: 51 additions & 53 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/sync-working-groups/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"start": "node ./sync-working-groups.ts"
},
"dependencies": {
"arktype": "^2.1.27"
"arktype": "2.1.28"
}
}
3 changes: 2 additions & 1 deletion src/_design-system/breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ export const Breadcrumbs = ({

const title = extractStringsFromReactNode(item.title)
const className = clsx(
"text-neu-700 dark:text-neu-400 min-w-6 last:text-neu-800 dark:last:text-neu-800 leading-none",
"text-neu-700 dark:text-neu-400 min-w-6 last:text-neu-800 dark:last:text-neu-800 leading-none whitespace-pre",
href &&
"gql-focus-visible ring-inset hover:text-neu-900 hover:underline underline-offset-2",
item.title.length > 8 ? "overflow-hidden truncate" : "shrink-0",
)

return (
Expand Down
61 changes: 61 additions & 0 deletions src/app/(main)/resources/[category]/blog-posts-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client"

import { Button } from "@/app/conf/_design-system/button"
import { Eyebrow } from "@/_design-system/eyebrow"
import { BlogCard } from "@/components/blog-page/blog-card"

export interface BlogPost {
href: string
title: string
author: string
date?: Date
tags: string[]
}

export interface BlogPostsSectionProps {
title: string
description: string
posts: BlogPost[]
readAllHref?: string
readAllLabel?: string
}

export function BlogPostsSection({
title,
description,
posts,
readAllHref = "/blog",
readAllLabel = "Read all GraphQL stories",
}: BlogPostsSectionProps) {
return (
<section className="gql-container gql-section flex flex-col gap-10 lg:gap-16">
<header className="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div className="flex flex-col gap-3">
<Eyebrow>Blog posts</Eyebrow>
<h2 className="typography-h2 max-w-[700px] text-pretty">{title}</h2>
<p className="typography-body-md max-w-[577px] text-neu-800">
{description}
</p>
</div>
<Button href={readAllHref} variant="secondary" size="md">
{readAllLabel}
</Button>
</header>

<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{posts.map(post => (
<BlogCard
key={post.href}
route={post.href}
frontMatter={{
title: post.title,
byline: post.author,
date: post.date ?? new Date(),
tags: post.tags,
}}
/>
))}
</div>
</section>
)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import path from "node:path"
import { glob } from "node:fs/promises"
import { readFile } from "node:fs/promises"
import matter from "gray-matter"

import type { CSSProperties } from "react"
import { Button } from "@/app/conf/_design-system/button"
import blurCorner from "./blur-corner.webp"
import { Eyebrow } from "@/_design-system/eyebrow"
import slugMap from "@/code/slug-map.json"
import { type Topic } from "@/resources/types"
import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"

import { icons } from "./icons"
import { ChevronRight } from "@/app/conf/_design-system/pixelarticons/chevron-right"

interface LibraryEntry {
name: string
href?: string
group: string
icon: React.ReactNode
tags: string[]
}

const librariesPromise = loadLibraries()

async function loadLibraries(): Promise<LibraryEntry[]> {
const entries: LibraryEntry[] = []

for await (const file of glob("src/code/**/*.md")) {
const relative = path.relative("src/code", file)
const segments = relative.split(path.sep)
const top = segments[0]
const group =
top === "language-support" ? (segments[1] ?? "language-support") : top
if (!group) continue

const raw = await readFile(file, "utf8")
const { data } = matter(raw)
const tags: string[] = Array.isArray(data.tags) ? data.tags : []
if (!tags.includes("tools-and-libraries")) continue

const name: string | undefined = data.name
if (!name) continue

const href: string | undefined =
data.url ??
(data.github ? `https://github.com/${data.github}` : undefined) ??
(data.npm ? `https://npmjs.com/package/${data.npm}` : undefined)

entries.push({ name, href, group, tags })
}

const deduped = entries.filter(
(item, index, self) =>
index ===
self.findIndex(t => t.name.toLowerCase() === item.name.toLowerCase()),
)

return deduped
}

function displayName(id: string) {
const key = id as keyof typeof slugMap
return slugMap[key] ?? id
}

export async function CategoryToolsLibrariesSection({
category,
}: {
category: Topic
}) {
const libraries = await librariesPromise
const filtered = libraries.filter(item => item.tags.includes(category))

const grouped = Array.from(
filtered.reduce<Map<string, LibraryEntry[]>>((acc, item) => {
const list = acc.get(item.group) ?? []
list.push(item)
acc.set(item.group, list)
return acc
}, new Map()),
)
.map(([group, items]) => ({
id: group,
name: displayName(group),
items: items
.sort((a, b) =>
a.name.localeCompare(b.name, "en", { sensitivity: "base" }),
)
.slice(0, 20),
}))
.sort((a, b) => b.items.length - a.items.length)

if (grouped.length === 0) {
return null
}

return (
<div className="relative bg-neu-100 dark:bg-neu-50/25">
<Stripes />
<section
id="tools-and-libraries"
className="gql-container gql-section relative flex flex-col gap-8 overflow-hidden"
>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-3">
<Eyebrow className="!text-pri-base dark:!text-pri-light">
key tools & libraries
</Eyebrow>
<h2 className="typography-h3 text-pretty">
Build GraphQL with tools and libraries
</h2>
<p className="typography-body-md text-neu-800">
Explore language and platform tooling to ship production-ready
graphs.
</p>
</div>
<Button href="/code" variant="primary" className="w-fit">
See all Tools & Libraries
</Button>
</div>

<div className="flex flex-wrap gap-4 pb-2 lg:overflow-visible">
{grouped.map((group, index) => {
const nextLength = grouped[index + 1]?.items.length ?? 0
const columns =
nextLength > 0 && group.items.length >= nextLength * 1.9 ? 2 : 1
const listStyle = { "--item-columns": columns } as CSSProperties
const breakIndex =
columns === 2 ? Math.floor(group.items.length / 2) : 0

const Icon = icons[group.id]

return (
<div
key={group.id}
className="min-w-[480px] shrink-0 grow border border-neu-200 bg-neu-50 dark:bg-neu-50/50 lg:w-1/3 lg:min-w-0"
>
<div className="typography-body-lg flex items-center gap-3 border-b border-inherit bg-neu-50 px-4 py-3 text-neu-900">
{Icon && (
<div className="border-r border-inherit p-3">
<Icon className="size-10" />
</div>
)}
{group.name}
<div className="border-l border-inherit p-3 max-md:hidden">
{/* TODO: On mobile */}
<ChevronRight className="rotate-90" />
</div>
</div>
<ul
className="gap-0 divide-y divide-neu-200 dark:divide-neu-100 lg:[column-count:var(--item-columns,1)]"
style={listStyle}
>
{group.items.map((item, i) => (
<li
key={`${group.id}-${item.name}`}
style={{
borderTop: breakIndex === i ? "none" : "",
borderLeftWidth: breakIndex >= i ? "1px" : "",
}}
>
{item.href ? (
<a
href={item.href}
className="flex items-center justify-between bg-neu-0/40 px-4 py-3 text-neu-900 transition-colors hover:bg-neu-0 hover:duration-0"
>
{item.name}
</a>
) : (
<span className="flex items-center justify-between bg-neu-50 px-4 py-3 text-neu-900">
{item.name}
</span>
)}
</li>
))}
</ul>
</div>
)
})}
</div>
</section>
</div>
)
}

function Stripes() {
return (
<div
className="pointer-events-none absolute inset-x-0 top-0 h-[542px]"
style={{
maskImage: `url(${blurCorner.src})`,
WebkitMaskImage: `url(${blurCorner.src})`,
maskSize: "62% 62%",
WebkitMaskSize: "62% 62%",
maskPosition: "top right",
WebkitMaskPosition: "top right",
maskRepeat: "no-repeat",
WebkitMaskRepeat: "no-repeat",
}}
>
<StripesDecoration
evenClassName="bg-[linear-gradient(90deg,hsl(var(--color-pri-lighter))_0_12px,hsl(var(--color-pri-light))_12px_24px)] dark:bg-[linear-gradient(90deg,hsl(var(--color-sec-dark)/0.22)_0_12px,hsl(var(--color-sec-base)/0.22)_12px_24px)]"
oddClassName="bg-[linear-gradient(90deg,hsl(var(--color-pri-light))_0_12px,hsl(var(--color-pri-base)/0)_12px_24px)] dark:bg-[linear-gradient(90deg,hsl(var(--color-sec-base)/0.14)_0_12px,hsl(var(--color-sec-light)/0.14)_12px_24px)]"
angle="-90deg"
/>
</div>
)
}
11 changes: 11 additions & 0 deletions src/app/(main)/resources/[category]/icons/ballerina.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/app/(main)/resources/[category]/icons/c-net.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/app/(main)/resources/[category]/icons/clojure.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/app/(main)/resources/[category]/icons/elixir.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/app/(main)/resources/[category]/icons/elm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/app/(main)/resources/[category]/icons/flutter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading