mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
changelog filters
This commit is contained in:
1 parent
953a3edf12
commit
23c5e62ab0
2 files changed
+294
-103
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { ChangelogLabel, ChangelogTimelineIndexItem } from '~/lib/changelog-github'
|
||||
import { githubChangelogLabelFilterUrl, githubLabelHex } from '~/lib/changelog.utils'
|
||||
import { githubChangelogLabelFilterUrl } from '~/lib/changelog.utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { GitCommit } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -24,7 +24,6 @@ function LabelBadges({
|
||||
}: {
|
||||
labels: ChangelogLabel[]
|
||||
onBadgeClick?: (e: MouseEvent) => void
|
||||
/** Minimal chip for dense list rows */
|
||||
tiny?: boolean
|
||||
}) {
|
||||
if (labels.length === 0) return null
|
||||
@@ -63,13 +62,18 @@ function TimelineRow({
|
||||
mode,
|
||||
href,
|
||||
onSelect,
|
||||
dateFormat = 'month-day',
|
||||
}: {
|
||||
item: ChangelogTimelineIndexItem
|
||||
mode: 'link' | 'action'
|
||||
href?: string
|
||||
onSelect?: (item: ChangelogTimelineIndexItem) => void
|
||||
dateFormat?: 'month-day' | 'full'
|
||||
}) {
|
||||
const dateNoYear = dayjs(item.sortDate).format('MMM D')
|
||||
const dateLabel =
|
||||
dateFormat === 'full'
|
||||
? dayjs(item.sortDate).format('MMM D, YYYY')
|
||||
: dayjs(item.sortDate).format('MMM D')
|
||||
const labels = item.labels ?? []
|
||||
|
||||
const titleBlock =
|
||||
@@ -87,7 +91,7 @@ function TimelineRow({
|
||||
dateTime={item.sortDate}
|
||||
className="text-foreground-lighter group-hover:text-foreground-light text-xs tracking-normal"
|
||||
>
|
||||
{dateNoYear}
|
||||
{dateLabel}
|
||||
</time>
|
||||
<LabelBadges labels={labels} onBadgeClick={(e) => e.stopPropagation()} />
|
||||
</div>
|
||||
@@ -118,12 +122,34 @@ function TimelineRow({
|
||||
)
|
||||
}
|
||||
|
||||
export function ChangelogV3TimelineFlatList(props: {
|
||||
items: ChangelogTimelineIndexItem[]
|
||||
mode: 'action'
|
||||
onSelect: (item: ChangelogTimelineIndexItem) => void
|
||||
showFullDate?: boolean
|
||||
}) {
|
||||
const { items, onSelect, showFullDate } = props
|
||||
const dateFormat = showFullDate ? 'full' : 'month-day'
|
||||
return (
|
||||
<div className="min-w-0 [&>*:last-child]:border-b-0" role="list">
|
||||
{items.map((item) => (
|
||||
<TimelineRow
|
||||
key={item.number}
|
||||
item={item}
|
||||
mode="action"
|
||||
onSelect={onSelect}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props =
|
||||
| {
|
||||
items: ChangelogTimelineIndexItem[]
|
||||
mode: 'link'
|
||||
hrefFor: (item: ChangelogTimelineIndexItem) => string
|
||||
/** When true, no left border / offset — use when an ancestor already draws the timeline line. */
|
||||
omitOuterTimelineBorder?: boolean
|
||||
}
|
||||
| {
|
||||
@@ -152,7 +178,7 @@ export function ChangelogV3TimelineList(props: Props) {
|
||||
>
|
||||
<h2
|
||||
id={`changelog-year-${year}`}
|
||||
className="border-default bg-default text-foreground-light sticky top-28 z-20 border-b py-2 pl-0 font-mono text-sm tracking-wide lg:hidden"
|
||||
className="border-default bg-default text-foreground-light sticky top-[65px] z-20 border-b py-2 pl-0 font-mono text-sm tracking-wide lg:hidden"
|
||||
>
|
||||
{year}
|
||||
</h2>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
|
||||
import { ChangelogV3TimelineList } from '~/components/Changelog/ChangelogV3TimelineList'
|
||||
import {
|
||||
ChangelogV3TimelineFlatList,
|
||||
ChangelogV3TimelineList,
|
||||
} from '~/components/Changelog/ChangelogV3TimelineList'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import {
|
||||
@@ -13,7 +16,7 @@ import { discussionDisplayDate, githubChangelogLabelFilterUrl } from '~/lib/chan
|
||||
import mdxComponents from '~/lib/mdx/mdxComponents'
|
||||
import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
|
||||
import dayjs from 'dayjs'
|
||||
import { GitCommit } from 'lucide-react'
|
||||
import { GitCommit, ListFilter, X } from 'lucide-react'
|
||||
import type { GetServerSideProps } from 'next'
|
||||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
@@ -24,18 +27,40 @@ import { NuqsAdapter } from 'nuqs/adapters/next/pages'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
cn,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
IconYCombinator,
|
||||
Input,
|
||||
Input_Shadcn_,
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import 'ui-patterns/ShimmeringLoader/index.css'
|
||||
|
||||
const FEATURED_COUNT = 10
|
||||
const FEATURED_COUNT = 5
|
||||
|
||||
/** GitHub changelog label slugs used for product filters. */
|
||||
const CHANGELOG_PRODUCT_TAGS = [
|
||||
{ slug: 'storage', label: 'Storage' },
|
||||
{ slug: 'database', label: 'Database' },
|
||||
{ slug: 'realtime', label: 'Realtime' },
|
||||
{ slug: 'edge-functions', label: 'Edge Functions' },
|
||||
{ slug: 'auth', label: 'Auth' },
|
||||
{ slug: 'sdk', label: 'SDK' },
|
||||
{ slug: 'self-hosted', label: 'Self-hosted' },
|
||||
{ slug: 'vector', label: 'Vector' },
|
||||
{ slug: 'billing', label: 'Billing' },
|
||||
{ slug: 'breaking-change', label: 'Breaking Change' },
|
||||
{ slug: 'cli', label: 'CLI' },
|
||||
{ slug: 'infra', label: 'Infra' },
|
||||
{ slug: 'dashboard', label: 'Dashboard' },
|
||||
{ slug: 'docs', label: 'Docs' },
|
||||
] as const
|
||||
|
||||
type FeaturedEntry = {
|
||||
number: number
|
||||
@@ -56,6 +81,8 @@ type ModalPayload = {
|
||||
type PageProps = {
|
||||
featured: FeaturedEntry[]
|
||||
restIndex: ChangelogTimelineIndexItem[]
|
||||
/** Full sorted index (visible only), for client-side filtering. */
|
||||
allIndex: ChangelogTimelineIndexItem[]
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<PageProps> = async ({ res }) => {
|
||||
@@ -64,6 +91,7 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({ res })
|
||||
try {
|
||||
const changelogIndex = await getChangelogTimelineSortedIndex()
|
||||
const visible = changelogIndex.filter((item) => !item.title.includes('[d]'))
|
||||
const allIndex = visible
|
||||
const firstMeta = visible.slice(0, FEATURED_COUNT)
|
||||
const restIndex = visible.slice(FEATURED_COUNT)
|
||||
|
||||
@@ -80,14 +108,15 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({ res })
|
||||
)
|
||||
if (!discussion) return null
|
||||
const source = await mdxSerialize(discussion.body)
|
||||
const created_at = discussionDisplayDate({
|
||||
title: discussion.title,
|
||||
createdAt: discussion.createdAt,
|
||||
})
|
||||
const created_at =
|
||||
discussionDisplayDate({
|
||||
title: discussion.title,
|
||||
createdAt: discussion.createdAt,
|
||||
}) ?? discussion.createdAt
|
||||
return {
|
||||
number: meta.number,
|
||||
title: discussion.title,
|
||||
url: discussion.url,
|
||||
url: discussion.url ?? '',
|
||||
created_at,
|
||||
source,
|
||||
labels: meta.labels,
|
||||
@@ -100,10 +129,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({ res })
|
||||
)
|
||||
).filter((e): e is FeaturedEntry => e != null)
|
||||
|
||||
return { props: { featured, restIndex } }
|
||||
return { props: { featured, restIndex, allIndex } }
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return { props: { featured: [], restIndex: [] } }
|
||||
return { props: { featured: [], restIndex: [], allIndex: [] } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +144,26 @@ export default function ChangelogProgressivePage(props: PageProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function ChangelogProgressiveContent({ featured, restIndex }: PageProps) {
|
||||
function itemMatchesSearch(item: ChangelogTimelineIndexItem, q: string) {
|
||||
const n = q.trim().toLowerCase()
|
||||
if (!n) return true
|
||||
if (item.title.toLowerCase().includes(n)) return true
|
||||
return item.labels.some((l) => l.name.toLowerCase().includes(n))
|
||||
}
|
||||
|
||||
function itemMatchesSelectedTags(
|
||||
item: ChangelogTimelineIndexItem,
|
||||
selected: Set<(typeof CHANGELOG_PRODUCT_TAGS)[number]['slug']>
|
||||
) {
|
||||
if (selected.size === 0) return true
|
||||
const labelNames = new Set(item.labels.map((l) => l.name.toLowerCase()))
|
||||
for (const slug of selected) {
|
||||
if (labelNames.has(slug.toLowerCase())) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function ChangelogProgressiveContent({ featured, restIndex, allIndex }: PageProps) {
|
||||
const [discussion, setDiscussion] = useQueryState(
|
||||
'discussion',
|
||||
parseAsInteger.withOptions({ shallow: true, history: 'push' })
|
||||
@@ -125,6 +173,36 @@ function ChangelogProgressiveContent({ featured, restIndex }: PageProps) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [filterPanelOpen, setFilterPanelOpen] = useState(false)
|
||||
const [filterSearch, setFilterSearch] = useState('')
|
||||
const [selectedTags, setSelectedTags] = useState<
|
||||
Set<(typeof CHANGELOG_PRODUCT_TAGS)[number]['slug']>
|
||||
>(() => new Set())
|
||||
|
||||
const filteredIndex = useMemo(() => {
|
||||
const q = filterSearch
|
||||
const hasSearch = q.trim().length > 0
|
||||
const hasTags = selectedTags.size > 0
|
||||
if (!hasSearch && !hasTags) return null
|
||||
return allIndex
|
||||
.filter((item) => itemMatchesSearch(item, q) && itemMatchesSelectedTags(item, selectedTags))
|
||||
.sort((a, b) => dayjs(b.sortDate).diff(dayjs(a.sortDate)))
|
||||
}, [allIndex, filterSearch, selectedTags])
|
||||
|
||||
const toggleProductTag = (slug: (typeof CHANGELOG_PRODUCT_TAGS)[number]['slug']) => {
|
||||
setSelectedTags((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(slug)) next.delete(slug)
|
||||
else next.add(slug)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilterSearch('')
|
||||
setSelectedTags(new Set())
|
||||
}
|
||||
|
||||
const preview = useMemo(() => {
|
||||
if (discussion == null) return null
|
||||
const fromFeatured = featured.find((e) => e.number === discussion)
|
||||
@@ -135,16 +213,16 @@ function ChangelogProgressiveContent({ featured, restIndex }: PageProps) {
|
||||
dateIso: fromFeatured.created_at,
|
||||
}
|
||||
}
|
||||
const fromRest = restIndex.find((e) => e.number === discussion)
|
||||
if (fromRest) {
|
||||
const fromIndex = allIndex.find((e) => e.number === discussion)
|
||||
if (fromIndex) {
|
||||
return {
|
||||
title: fromRest.title,
|
||||
url: fromRest.url,
|
||||
dateIso: fromRest.sortDate,
|
||||
title: fromIndex.title,
|
||||
url: fromIndex.url,
|
||||
dateIso: fromIndex.sortDate,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [discussion, featured, restIndex])
|
||||
}, [discussion, featured, allIndex])
|
||||
|
||||
useEffect(() => {
|
||||
if (discussion == null) {
|
||||
@@ -231,90 +309,177 @@ function ChangelogProgressiveContent({ featured, restIndex }: PageProps) {
|
||||
<p className="text-foreground-lighter text-lg">
|
||||
New updates and product improvements
|
||||
</p>
|
||||
<ChangelogRssButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="border-muted relative lg:ml-2 lg:border-l lg:pl-8"
|
||||
aria-label="Changelog timeline"
|
||||
>
|
||||
<div className="grid">
|
||||
{featured.map((entry) => (
|
||||
<div
|
||||
key={entry.number}
|
||||
id={entry.number.toString()}
|
||||
className="grid pb-12 lg:grid-cols-12 lg:gap-8 lg:pb-36 scroll-mt-32"
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
type={filterPanelOpen ? 'default' : 'text'}
|
||||
size="tiny"
|
||||
className="shrink-0"
|
||||
aria-expanded={filterPanelOpen}
|
||||
aria-controls="changelog-progressive-filters"
|
||||
icon={
|
||||
filterPanelOpen ? (
|
||||
<X className="h-4 w-4" strokeWidth={1.5} aria-hidden />
|
||||
) : (
|
||||
<ListFilter className="h-4 w-4" strokeWidth={1.5} aria-hidden />
|
||||
)
|
||||
}
|
||||
onClick={() => setFilterPanelOpen((o) => !o)}
|
||||
>
|
||||
<div className="col-span-12 -ml-[31px] mb-8 lg:mb-0 self-start lg:sticky lg:top-0 lg:col-span-4 lg:-mt-32 lg:pt-32">
|
||||
<div className="flex w-full items-baseline border-b pb-4 lg:gap-4 lg:border-none lg:pb-0">
|
||||
<div className="hidden lg:flex bg-border border-muted text-foreground-lighter -ml-2.5 h-5 w-5 items-center justify-center rounded border drop-shadow-sm">
|
||||
<GitCommit size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{entry.title && (
|
||||
<Link href={entry.url}>
|
||||
<h3 className="text-foreground text-lg">{entry.title}</h3>
|
||||
</Link>
|
||||
)}
|
||||
<p className="text-foreground-lighter font-mono text-xs">
|
||||
{dayjs(entry.created_at).format('MMM D, YYYY')}
|
||||
</p>
|
||||
{entry.labels && entry.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-1.5">
|
||||
{entry.labels.map((label) => (
|
||||
<a
|
||||
key={`${entry.number}-${label.name}`}
|
||||
href={githubChangelogLabelFilterUrl(label.name)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group inline-flex no-underline focus-visible:ring-brand-default rounded-md focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
<Badge className="group-hover:text-foreground-light text-foreground-lighter group-hover:border-foreground-muted px-1.5 py-px text-[11px] lowercase">
|
||||
{label.name}
|
||||
</Badge>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 lg:max-w-[calc(100vw-80px)]">
|
||||
<article className="prose prose-docs max-w-none [overflow-wrap:break-word]">
|
||||
<MDXRemote {...entry.source} components={mdxComponents('blog')} />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{restIndex.length > 0 && (
|
||||
<section aria-label="Earlier changelog entries" className="lg:pb-28">
|
||||
<ChangelogV3TimelineList
|
||||
items={restIndex}
|
||||
mode="action"
|
||||
onSelect={handleSelectFromList}
|
||||
omitOuterTimelineBorder
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<div className="hidden lg:grid">
|
||||
<div className="col-span-12 -ml-8 mb-8 lg:mb-0 self-start lg:sticky lg:top-0 lg:col-span-4 lg:-mt-32 lg:pt-32">
|
||||
<div className="flex w-full items-baseline border-b pb-4 lg:gap-4 lg:border-none lg:pb-0">
|
||||
<Link
|
||||
href="https://www.ycombinator.com/companies/supabase"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden lg:flex -ml-2 text-foreground-lighter hover:text-foreground"
|
||||
title="YCombinator — Summer 2020"
|
||||
>
|
||||
<IconYCombinator size={16} className="text-current" />
|
||||
</Link>
|
||||
</div>
|
||||
{filterPanelOpen ? 'Hide filters' : 'Filter changelog'}
|
||||
</Button>
|
||||
<ChangelogRssButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterPanelOpen && (
|
||||
<div id="changelog-progressive-filters" className="flex flex-col gap-2 -mt-4 sm:mx-0">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<label htmlFor="changelog-filter-search" className="sr-only">
|
||||
Search changelog
|
||||
</label>
|
||||
<Input_Shadcn_
|
||||
id="changelog-filter-search"
|
||||
size="small"
|
||||
placeholder="Search changelog..."
|
||||
value={filterSearch}
|
||||
onChange={(e) => setFilterSearch(e.target.value)}
|
||||
/>
|
||||
{(filterSearch.trim().length > 0 || selectedTags.size > 0) && (
|
||||
<Button
|
||||
type="outline"
|
||||
size="tiny"
|
||||
className="absolute inset-1 my-auto left-auto shrink-0"
|
||||
onClick={clearFilters}
|
||||
icon={<X className="h-4 w-4" strokeWidth={1.5} aria-hidden />}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="sr-only">Filter by tags</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{CHANGELOG_PRODUCT_TAGS.map(({ slug, label }) => {
|
||||
const on = selectedTags.has(slug)
|
||||
return (
|
||||
<button key={slug} type="button" onClick={() => toggleProductTag(slug)}>
|
||||
<Badge
|
||||
variant={on ? 'success' : 'default'}
|
||||
className={cn(!on && 'hover:text-foreground')}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredIndex != null ? (
|
||||
<section aria-label="Filtered changelog entries" className="min-w-0">
|
||||
{filteredIndex.length === 0 ? (
|
||||
<p className="text-foreground-lighter text-sm">No entries match your filters.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-foreground-lighter mb-3 text-sm">
|
||||
{filteredIndex.length} {filteredIndex.length === 1 ? 'result' : 'results'}
|
||||
</p>
|
||||
<ChangelogV3TimelineFlatList
|
||||
items={filteredIndex}
|
||||
mode="action"
|
||||
onSelect={handleSelectFromList}
|
||||
showFullDate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<div
|
||||
className="border-muted relative lg:ml-2 lg:border-l lg:pl-8"
|
||||
aria-label="Changelog timeline"
|
||||
>
|
||||
<div className="grid">
|
||||
{featured.map((entry) => (
|
||||
<div
|
||||
key={entry.number}
|
||||
id={entry.number.toString()}
|
||||
className="grid pb-12 lg:grid-cols-12 lg:gap-8 lg:pb-36 scroll-mt-32"
|
||||
>
|
||||
<div className="col-span-12 lg:-ml-[31px] mb-8 lg:mb-0 self-start lg:sticky lg:top-32 lg:col-span-4">
|
||||
<div className="flex w-full items-baseline border-b pb-4 lg:gap-4 lg:border-none lg:pb-0">
|
||||
<div className="hidden lg:flex bg-border border-muted text-foreground-lighter -ml-2.5 h-5 w-5 items-center justify-center rounded border drop-shadow-sm">
|
||||
<GitCommit size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{entry.title && (
|
||||
<Link href={entry.url}>
|
||||
<h3 className="text-foreground text-lg">{entry.title}</h3>
|
||||
</Link>
|
||||
)}
|
||||
<p className="text-foreground-lighter font-mono text-xs">
|
||||
{dayjs(entry.created_at).format('MMM D, YYYY')}
|
||||
</p>
|
||||
{entry.labels && entry.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-1.5">
|
||||
{entry.labels.map((label) => (
|
||||
<a
|
||||
key={`${entry.number}-${label.name}`}
|
||||
href={githubChangelogLabelFilterUrl(label.name)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group inline-flex no-underline focus-visible:ring-brand-default rounded-md focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
<Badge className="group-hover:text-foreground-light text-foreground-lighter group-hover:border-foreground-muted px-1.5 py-px text-[11px] lowercase">
|
||||
{label.name}
|
||||
</Badge>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 lg:max-w-[calc(100vw-80px)]">
|
||||
<article className="prose prose-docs max-w-none [overflow-wrap:break-word]">
|
||||
<MDXRemote {...entry.source} components={mdxComponents('blog')} />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{restIndex.length > 0 && (
|
||||
<section aria-label="Earlier changelog entries" className="lg:pb-28">
|
||||
<ChangelogV3TimelineList
|
||||
items={restIndex}
|
||||
mode="action"
|
||||
onSelect={handleSelectFromList}
|
||||
omitOuterTimelineBorder
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<div className="hidden lg:grid">
|
||||
<div className="col-span-12 -ml-8 mb-8 lg:mb-0 self-start lg:sticky lg:top-0 lg:col-span-4 lg:-mt-32 lg:pt-32">
|
||||
<div className="flex w-full items-baseline border-b pb-4 lg:gap-4 lg:border-none lg:pb-0">
|
||||
<Link
|
||||
href="https://www.ycombinator.com/companies/supabase"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden lg:flex -ml-2 text-foreground-lighter hover:text-foreground"
|
||||
title="YCombinator — Summer 2020"
|
||||
>
|
||||
<IconYCombinator size={16} className="text-current" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CTABanner />
|
||||
</DefaultLayout>
|
||||
|
||||
Reference in new issue
Block a user