Skip to content

Commit

Permalink
Resources page (#780)
Browse files Browse the repository at this point in the history
Co-authored-by: Joss Mackison <[email protected]>
Co-authored-by: Emma Hamilton <[email protected]>
  • Loading branch information
3 people authored Nov 24, 2023
1 parent 0ca7f47 commit c94edf0
Show file tree
Hide file tree
Showing 13 changed files with 473 additions and 1 deletion.
70 changes: 69 additions & 1 deletion docs/keystatic.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default config({
name: 'Keystatic Docs',
},
navigation: {
Pages: ['pages', 'blog', 'projects'],
Pages: ['pages', 'blog', 'projects', 'resources'],
Config: ['authors', 'navigation'],
Experimental: ['pagesWithMarkdocField'],
},
Expand Down Expand Up @@ -261,6 +261,74 @@ export default config({
},
}),

// ------------------------------
// Resources
// ------------------------------
resources: collection({
label: 'Resources',
path: 'src/content/resources/*',
slugField: 'title',
schema: {
title: fields.slug({ name: { label: 'Title' } }),
type: fields.conditional(
fields.select({
label: 'Resource type',
options: [
{ label: 'YouTube video', value: 'youtube-video' },
{ label: 'Article', value: 'article' },
],
defaultValue: 'youtube-video',
}),
{
'youtube-video': fields.object({
videoId: fields.text({
label: 'Video ID',
description: 'The ID of the video (not the URL!)',
validation: { length: { min: 1 } },
}),
thumbnail: fields.cloudImage({
label: 'Video thumbnail',
description: 'A 16/9 thumbnail image for the video.',
}),
kind: fields.select({
label: 'Video kind',
options: [
{ label: 'Talk', value: 'talk' },
{ label: 'Screencast', value: 'screencast' },
],
defaultValue: 'screencast',
}),
description: fields.text({
label: 'Video description',
multiline: true,
validation: { length: { min: 1 } },
}),
}),
article: fields.object({
url: fields.url({
label: 'Article URL',
validation: { isRequired: true },
}),
authorName: fields.text({
label: 'Author name',
validation: { length: { min: 1 } },
}),
description: fields.text({
label: 'Article description',
multiline: true,
}),
}),
}
),
sortIndex: fields.integer({
label: 'Sort index',
description:
'A number value to sort items (low to high) on the front end.',
defaultValue: 10,
}),
},
}),

// ------------------------------
// For testing purposes only
// ------------------------------
Expand Down
48 changes: 48 additions & 0 deletions docs/src/app/(public)/resources/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Main } from '../../../components/main';
import Footer from '../../../components/footer';

export const metadata = {
title: {
template: '%s - Resources | Keystatic',
default: 'Resources',
},
description:
'A collection of videos, talks, articles and other resources to help you learn Keystatic and dig deeper.',
openGraph: {
title: 'Resources',
description:
'A collection of videos, talks, articles and other resources to help you learn Keystatic and dig deeper.',
images: [
{
url: '/og?title=Resources',
},
],
siteName: 'Keystatic',
type: 'website',
url: 'https://keystatic.com/resources',
},
twitter: {
card: 'summary_large_image',
title: 'Resources',
description:
'A collection of videos, talks, articles and other resources to help you learn Keystatic and dig deeper.',
site: '@thekeystatic',
},
};

export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<div className="mx-auto min-h-screen w-full max-w-7xl px-6">
<Main className="flex gap-8">
<div className="flex-1">{children}</div>
</Main>
</div>
<Footer />
</>
);
}
213 changes: 213 additions & 0 deletions docs/src/app/(public)/resources/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';

import { ArrowRightIcon } from '../../../components/icons/arrow-right';
import { reader } from '../../../utils/reader';
import Button from '../../../components/button';
import { Entry } from '@keystatic/core/reader';
import keystaticConfig from '../../../../keystatic.config';

type ResourceEntry = Entry<
(typeof keystaticConfig)['collections']['resources']
>;

type VideoProps = {
title: ResourceEntry['title'];
} & Omit<
Extract<ResourceEntry['type'], { discriminant: 'youtube-video' }>['value'],
'kind'
>;

type ArticleProps = {
title: ResourceEntry['title'];
} & Omit<
Extract<ResourceEntry['type'], { discriminant: 'article' }>['value'],
'kind'
>;

export default async function Resources() {
const resources = await reader().collections.resources.all();
if (!resources) notFound();

const sortedVideos = resources
.filter(
resource =>
resource.entry.type.discriminant === 'youtube-video' &&
resource.entry.type.value.kind === 'screencast'
)
.sort((a, b) => {
return (a.entry.sortIndex as number) - (b.entry.sortIndex as number);
})
.map(resource => ({
title: resource.entry.title,
sortIndex: resource.entry.sortIndex,
...resource.entry.type.value,
})) as VideoProps[];

const sortedTalks = resources
.filter(
resource =>
resource.entry.type.discriminant === 'youtube-video' &&
resource.entry.type.value.kind === 'talk'
)
.sort((a, b) => {
return (a.entry.sortIndex as number) - (b.entry.sortIndex as number);
})
.map(resource => ({
title: resource.entry.title,
sortIndex: resource.entry.sortIndex,
...resource.entry.type.value,
})) as VideoProps[];

const sortedArticles = resources
.filter(resource => resource.entry.type.discriminant === 'article')
.sort((a, b) => {
return (a.entry.sortIndex as number) - (b.entry.sortIndex as number);
})
.map(resource => ({
title: resource.entry.title,
sortIndex: resource.entry.sortIndex,
...resource.entry.type.value,
})) as ArticleProps[];

return (
<div className="mt-24 pt-10">
<header className="mx-auto max-w-2xl text-center">
<h1 className="text-4xl font-medium md:text-5xl">Resources</h1>
<div className="mt-6 space-y-4 text-lg">
<p>
A collection of videos, talks, articles and other resources to help
you learn Keystatic and dig deeper.
</p>
</div>
</header>

<div className="mt-12 divide-y divide-slate-5">
<Section title="YouTube Videos">
<p>
The{' '}
<Link
href="https://youtube.com/@thinkmill"
className="underline hover:no-underline"
>
Thinkmill channel
</Link>{' '}
has a growing collection of content about Keystatic.
</p>
<ResourceGrid>
{sortedVideos.map(video => (
<Video
title={video.title}
videoId={video.videoId}
description={video.description}
thumbnail={video.thumbnail}
/>
))}
</ResourceGrid>
<Button
variant="regular"
impact="light"
className="mt-12 inline-flex items-center gap-2"
href="https://www.youtube.com/playlist?list=PLYyvXL46d-pzqwOKdofd5aKiqPTAN3ql6"
>
<span>Watch more videos</span>
<ArrowRightIcon />
</Button>
</Section>
<Section title="Talks">
<p>Recorded Keystatic talks from local meetups and conferences.</p>
<ResourceGrid>
{sortedTalks.map(video => (
<Video
videoId={video.videoId}
title={video.title}
description={video.description}
thumbnail={video.thumbnail}
/>
))}
</ResourceGrid>
</Section>
<Section title="Articles">
<ResourceGrid>
{sortedArticles.map(article => (
<li className="mb-4 mr-4">
<h3 className="text-xl font-medium">
<Link href={article.url} className="hover:underline">
{article.title}
</Link>
</h3>
<p className="mt-1 text-sm text-slate-10">
by {article.authorName}
</p>
{article.description && (
<p className="mt-4">{article.description}</p>
)}
</li>
))}
</ResourceGrid>
</Section>

<Section>
<div className="inline-flex flex-col gap-4 rounded-lg bg-slate-3 px-4 py-6 sm:flex-row">
<div className="flex h-6 items-center text-3xl"></div>
<div className="flex flex-col gap-3">
<p className="text-md text-slate-12">
This page is a work in progress — more resources coming soon!
</p>
</div>
</div>
</Section>
</div>
</div>
);
}

function Video({ videoId, title, description, thumbnail }: VideoProps) {
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
return (
<li>
<Link
href={videoUrl}
className="group relative block aspect-video w-full"
>
<Image
fill
src={thumbnail.src}
alt={thumbnail.alt || ''}
className="h-full-w-full absolute inset-0 rounded-lg shadow-md transition-shadow group-hover:shadow-sm"
/>
</Link>
<h3 className="mt-6 text-xl font-medium">
<Link href={videoUrl} className="hover:underline">
{title}
</Link>
</h3>
<p className="mt-2">{description}</p>
</li>
);
}

function ResourceGrid(props: React.ComponentProps<'ul'>) {
return (
<ul
className="mt-8 grid items-start gap-x-6 gap-y-10 md:grid-cols-2 lg:grid-cols-3"
{...props}
/>
);
}

type SectionProps = {
title?: string | React.ReactNode;
introText?: string;
children: React.ReactNode;
};

function Section({ title, children }: SectionProps) {
return (
<section className="py-16">
{title && <h2 className="mb-4 text-2xl font-medium">{title}</h2>}
{children}
</section>
);
}
22 changes: 22 additions & 0 deletions docs/src/app/(public)/showcase/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ export const metadata = {
template: '%s - Showcase | Keystatic',
default: 'Showcase',
},
description:
'A collection of projects using Keystatic to manage parts of their codebase.',
openGraph: {
title: 'Showcase',
description:
'A collection of projects using Keystatic to manage parts of their codebase.',
images: [
{
url: '/og?title=Showcase',
},
],
siteName: 'Keystatic',
type: 'website',
url: 'https://keystatic.com/showcase',
},
twitter: {
card: 'summary_large_image',
title: 'Showcase',
description:
'A collection of projects using Keystatic to manage parts of their codebase.',
site: '@thekeystatic',
},
};

export default async function RootLayout({
Expand Down
5 changes: 5 additions & 0 deletions docs/src/content/navigation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ navGroups:
discriminant: page
value: quick-start
isNew: false
- label: Resources
link:
discriminant: url
value: /resources
isNew: true
- groupName: Integration guides
items:
- label: Astro
Expand Down
Loading

2 comments on commit c94edf0

@vercel
Copy link

@vercel vercel bot commented on c94edf0 Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

keystatic – ./dev-projects/next-app

keystatic-thinkmill-labs.vercel.app
keystatic.vercel.app
keystatic-git-main-thinkmill-labs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c94edf0 Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.