diff --git a/apps/cms/package.json b/apps/cms/package.json index 335627c4..4d11d289 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -15,10 +15,12 @@ "sanity" ], "dependencies": { - "@sanity/vision": "^3.46.0", + "@sanity/vision": "^3.48.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "sanity": "^3.46.0", + "react-player": "^2.16.0", + "sanity": "^3.48.0", + "sanity-plugin-media": "^2.2.5", "styled-components": "^6.1.8" }, "devDependencies": { diff --git a/apps/cms/sanity.config.ts b/apps/cms/sanity.config.ts index 87d96008..e5f6b4a6 100644 --- a/apps/cms/sanity.config.ts +++ b/apps/cms/sanity.config.ts @@ -2,6 +2,8 @@ import {defineConfig} from 'sanity' import {structureTool} from 'sanity/structure' import {visionTool} from '@sanity/vision' import {schemaTypes} from './schemaTypes' +import {codeInput} from '@sanity/code-input' +import {media} from 'sanity-plugin-media' export default defineConfig({ name: 'default', @@ -10,7 +12,7 @@ export default defineConfig({ projectId: '2fn86m3z', dataset: 'production', - plugins: [structureTool(), visionTool()], + plugins: [structureTool(), visionTool(), codeInput(), media()], schema: { types: schemaTypes, diff --git a/apps/cms/schemaTypes/author.ts b/apps/cms/schemaTypes/author.ts index e380781f..2b1ba2d6 100644 --- a/apps/cms/schemaTypes/author.ts +++ b/apps/cms/schemaTypes/author.ts @@ -40,6 +40,11 @@ export default defineType({ }, ], }), + defineField({ + name: 'twitter', + title: 'Twitter', + type: 'string', + }), ], preview: { select: { diff --git a/apps/cms/schemaTypes/blockContent.ts b/apps/cms/schemaTypes/blockContent.ts deleted file mode 100644 index 65384ba8..00000000 --- a/apps/cms/schemaTypes/blockContent.ts +++ /dev/null @@ -1,67 +0,0 @@ -import {defineType, defineArrayMember} from 'sanity' - -/** - * This is the schema definition for the rich text fields used for - * for this blog studio. When you import it in schemas.js it can be - * reused in other parts of the studio with: - * { - * name: 'someName', - * title: 'Some title', - * type: 'blockContent' - * } - */ -export default defineType({ - title: 'Block Content', - name: 'blockContent', - type: 'array', - of: [ - defineArrayMember({ - title: 'Block', - type: 'block', - // Styles let you set what your user can mark up blocks with. These - // correspond with HTML tags, but you can set any title or value - // you want and decide how you want to deal with it where you want to - // use your content. - styles: [ - {title: 'Normal', value: 'normal'}, - {title: 'H1', value: 'h1'}, - {title: 'H2', value: 'h2'}, - {title: 'H3', value: 'h3'}, - {title: 'H4', value: 'h4'}, - {title: 'Quote', value: 'blockquote'}, - ], - lists: [{title: 'Bullet', value: 'bullet'}], - // Marks let you mark up inline text in the block editor. - marks: { - // Decorators usually describe a single property – e.g. a typographic - // preference or highlighting by editors. - decorators: [ - {title: 'Strong', value: 'strong'}, - {title: 'Emphasis', value: 'em'}, - ], - // Annotations can be any object structure – e.g. a link or a footnote. - annotations: [ - { - title: 'URL', - name: 'link', - type: 'object', - fields: [ - { - title: 'URL', - name: 'href', - type: 'url', - }, - ], - }, - ], - }, - }), - // You can add additional types here. Note that you can't use - // primitive types such as 'string' and 'number' in the same array - // as a block type. - defineArrayMember({ - type: 'image', - options: {hotspot: true}, - }), - ], -}) diff --git a/apps/cms/schemaTypes/blockContent.tsx b/apps/cms/schemaTypes/blockContent.tsx new file mode 100644 index 00000000..b74c8ec1 --- /dev/null +++ b/apps/cms/schemaTypes/blockContent.tsx @@ -0,0 +1,242 @@ +import SanityImageUrlBuilder from '@sanity/image-url' +import {defineType, defineArrayMember, defineField, PreviewProps} from 'sanity' +import {PlayIcon, TwitterIcon, ImageIcon, DocumentIcon, LinkIcon} from '@sanity/icons' +import {Flex, Text, Box} from '@sanity/ui' +import YouTubePlayer from 'react-player/youtube' + +const imageUrlBuilder = SanityImageUrlBuilder({ + projectId: '2fn86m3z', + dataset: 'production', +}) + +export function YouTubePreview(props: PreviewProps) { + const {title: url} = props + + return ( + + {typeof url === 'string' ? : Add a YouTube URL} + + ) +} + +export function ImagePreview({image, large, caption}: any) { + const url = image ? imageUrlBuilder.image(image).width(600).url() : '' + + return ( + + {url ? ( + + ) : ( + + )} +
+ + {large ? 'Large' : 'Fit'} - {caption ? caption : 'No caption'} + +
+
+ ) +} + +export default defineType({ + title: 'Block Content', + name: 'blockContent', + type: 'array', + of: [ + defineArrayMember({ + title: 'Block', + type: 'block', + // Styles let you set what your user can mark up blocks with. These + // correspond with HTML tags, but you can set any title or value + // you want and decide how you want to deal with it where you want to + // use your content. + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'H3', value: 'h3'}, + {title: 'H4', value: 'h4'}, + {title: 'Quote', value: 'blockquote'}, + ], + lists: [ + {title: 'Bullet', value: 'bullet'}, + {title: 'Numbered', value: 'number'}, + ], + // Marks let you mark up inline text in the block editor. + marks: { + // Decorators usually describe a single property – e.g. a typographic + // preference or highlighting by editors. + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Code', value: 'code'}, + ], + // Annotations can be any object structure – e.g. a link or a footnote. + annotations: [ + { + title: 'URL', + name: 'link', + type: 'object', + fields: [ + { + title: 'URL', + name: 'href', + type: 'url', + }, + ], + }, + ], + }, + }), + // You can add additional types here. Note that you can't use + // primitive types such as 'string' and 'number' in the same array + // as a block type. + defineArrayMember({ + type: 'code', + name: 'code', + title: 'Code', + }), + defineArrayMember({ + type: 'object', + name: 'image-block', + title: 'Image', + icon: ImageIcon, + components: { + preview: ImagePreview, + }, + fields: [ + { + type: 'image', + name: 'image', + title: 'Image', + }, + { + type: 'boolean', + name: 'large', + title: 'Is large?', + initialValue: false, + }, + { + type: 'string', + name: 'caption', + title: 'Caption', + }, + ], + preview: { + select: { + image: 'image', + large: 'large', + caption: 'caption', + }, + }, + }), + defineArrayMember({ + type: 'object', + name: 'tweet', + title: 'Tweet', + icon: TwitterIcon, + fields: [ + { + type: 'string', + name: 'tweetId', + title: 'Tweet ID', + }, + ], + }), + defineArrayMember({ + type: 'object', + name: 'video', + title: 'Video', + icon: PlayIcon, + fields: [ + { + type: 'file', + name: 'videoFile', + title: 'Video File', + }, + { + type: 'string', + name: 'caption', + title: 'Caption', + }, + ], + }), + defineArrayMember({ + name: 'youtube', + type: 'object', + title: 'YouTube', + icon: PlayIcon, + fields: [ + defineField({ + name: 'url', + type: 'url', + title: 'YouTube video URL', + }), + ], + preview: { + select: {title: 'url'}, + }, + components: { + preview: YouTubePreview, + }, + }), + defineArrayMember({ + name: 'post-block', + type: 'object', + title: 'Post', + icon: DocumentIcon, + fields: [ + defineField({ + name: 'post', + type: 'reference', + title: 'Post', + to: [{type: 'post'}], + }), + ], + preview: { + select: { + title: 'post.title', + subtitle: 'post.subtitle', + media: 'post.mainImage', + }, + }, + }), + defineArrayMember({ + name: 'link-block', + type: 'object', + title: 'Link', + icon: LinkIcon, + fields: [ + defineField({ + name: 'url', + type: 'url', + title: 'Url', + }), + ], + preview: { + select: { + title: 'url', + }, + prepare({title}) { + return { + title, + subtitle: 'We will show a preview of the link in the blog post.', + } + }, + }, + }), + ], +}) diff --git a/apps/cms/schemaTypes/category.ts b/apps/cms/schemaTypes/category.ts deleted file mode 100644 index b7504a8d..00000000 --- a/apps/cms/schemaTypes/category.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {defineField, defineType} from 'sanity' - -export default defineType({ - name: 'category', - title: 'Category', - type: 'document', - fields: [ - defineField({ - name: 'title', - title: 'Title', - type: 'string', - }), - defineField({ - name: 'description', - title: 'Description', - type: 'text', - }), - ], -}) diff --git a/apps/cms/schemaTypes/index.ts b/apps/cms/schemaTypes/index.ts index e7fc24e6..6b057104 100644 --- a/apps/cms/schemaTypes/index.ts +++ b/apps/cms/schemaTypes/index.ts @@ -1,6 +1,6 @@ import blockContent from './blockContent' -import category from './category' +import tag from './tag' import post from './post' import author from './author' -export const schemaTypes = [post, author, category, blockContent] +export const schemaTypes = [post, author, tag, blockContent] diff --git a/apps/cms/schemaTypes/post.ts b/apps/cms/schemaTypes/post.ts index 16d9bd3d..21c440a2 100644 --- a/apps/cms/schemaTypes/post.ts +++ b/apps/cms/schemaTypes/post.ts @@ -10,6 +10,11 @@ export default defineType({ title: 'Title', type: 'string', }), + defineField({ + name: 'subtitle', + title: 'Subtitle', + type: 'string', + }), defineField({ name: 'slug', title: 'Slug', @@ -20,10 +25,10 @@ export default defineType({ }, }), defineField({ - name: 'author', - title: 'Author', - type: 'reference', - to: {type: 'author'}, + name: 'authors', + title: 'Authors', + type: 'array', + of: [{type: 'reference', to: {type: 'author'}}], }), defineField({ name: 'mainImage', @@ -34,10 +39,18 @@ export default defineType({ }, }), defineField({ - name: 'categories', - title: 'Categories', + name: 'tags', + title: 'Tags', type: 'array', - of: [{type: 'reference', to: {type: 'category'}}], + of: [{type: 'reference', to: {type: 'tag'}}], + }), + defineField({ + name: 'whatsNew', + title: "What's new", + description: + 'If this is set to true, this post will appear in the "What\'s new" section in Storybook?', + type: 'boolean', + initialValue: false, }), defineField({ name: 'publishedAt', @@ -54,7 +67,7 @@ export default defineType({ preview: { select: { title: 'title', - author: 'author.name', + author: 'authors.0.name', media: 'mainImage', }, prepare(selection) { diff --git a/apps/cms/schemaTypes/tag.ts b/apps/cms/schemaTypes/tag.ts new file mode 100644 index 00000000..7367644f --- /dev/null +++ b/apps/cms/schemaTypes/tag.ts @@ -0,0 +1,23 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'tag', + title: 'Tag', + type: 'document', + fields: [ + defineField({ + name: 'name', + title: 'Name', + type: 'string', + }), + defineField({ + name: 'slug', + title: 'Slug', + type: 'slug', + options: { + source: 'name', + maxLength: 96, + }, + }), + ], +}) diff --git a/apps/frontpage/app/blog/[slug]/page.tsx b/apps/frontpage/app/blog/[slug]/page.tsx new file mode 100644 index 00000000..2dc18daa --- /dev/null +++ b/apps/frontpage/app/blog/[slug]/page.tsx @@ -0,0 +1,296 @@ +import { notFound } from 'next/navigation'; +import { client, urlFor } from '../../../lib/sanity/client'; +import { Post } from '../page'; +import Image from 'next/image'; +import { format, parseISO } from 'date-fns'; +import Body from '../../../components/blog/body'; +import { NewsletterForm, Pill, SubHeader } from '@repo/ui'; +import { DiscordIcon, GithubIcon, XIcon, YoutubeIcon } from '@storybook/icons'; +import Link from 'next/link'; +import CopyButton from '../../../components/blog/copy-button'; + +interface PageProps { + params: { + slug: string[]; + }; +} + +export const generateStaticParams = async () => { + const posts = await client.fetch(`*[_type == "post"] { slug }`); + + const paths = posts.map((post) => ({ + slug: post.slug?.current || '', + })); + + return paths; +}; + +interface PostWithPrevNext extends Post { + prev: Post[]; + next: Post[]; +} + +export default async function Page({ params: { slug } }: PageProps) { + const post = await client.fetch( + `*[_type == "post" && slug.current == $slug][0]{ + ..., + authors[]->, + tags[]->, + body[]{ + ..., + _type == 'post-block' => { + post-> { + mainImage, + title, + slug, + subtitle, + authors[]-> + } + } + }, + 'prev': *[_type == 'post' && !(_id in path('drafts.**')) && _createdAt < ^._createdAt]{..., authors[]->} | order(_createdAt desc)[0..2], + 'next': *[_type == 'post' && !(_id in path('drafts.**')) && _createdAt > ^._createdAt]{..., authors[]->} | order(_createdAt desc)[0..2] + }`, + { slug }, + ); + + if (!post) notFound(); + + const img = post.mainImage; + const imageUrl = img && urlFor(img).url(); + const blurUrl = img && urlFor(img).width(20).quality(20).url(); + const keepReadingPosts = [...post.prev, ...post.next]; + const postUrl = `https://storybook.js.org/blog/${post.slug?.current}`; + + return ( + <> + + Join the community +
+ +
+
+ +
+
+ +
+
+ +
+ + } + /> +
+ {post?.tags && post.tags.length > 0 && ( +
+
+ {post.tags?.[0].name} +
+
+ )} +

+ {post.title} +

+
+ {post.subtitle} +
+
+
+
+ {post.authors?.map((author) => { + const img = author.image; + const imageUrl = img && urlFor(img).url(); + return ( +
+ {imageUrl && ( + My Image + )} +
+ ); + })} +
+
+ {post.authors?.map((author) => { + return ( + <> + and + {author.name} + + ); + })} +
+
+ +
+
+ {imageUrl && blurUrl && ( + My Image + )} +
+
+ {post.body && } +
+
+ Tags +
+ {post.tags?.map((tag) => ( + + + {tag.name} + + + ))} +
+
+
+ Last updated + +
+
+ +
+
+
+
+
+
+ Join the Storybook mailing list +
+
+ Get the latest news, updates and releases +
+
+ +
+
+
We're hiring!
+
+ Join the team behind Storybook and Chromatic. Build tools that are + used in production by 100s of thousands of developers. Remote-first. +
+ +
+
+
+
Keep reading
+
+ {keepReadingPosts && + keepReadingPosts.slice(0, 3).map((post) => { + const img = post.mainImage; + const imageUrl = img && urlFor(img).url(); + const blurUrl = img && urlFor(img).width(20).quality(20).url(); + const url = `/blog/${post?.slug?.current}`; + + return ( +
+ {imageUrl && blurUrl && ( + + My Image + + )} + + {post?.title} + +
+ {post?.subtitle} +
+
+
+
+ {post.authors?.map((author) => { + const img = author.image; + const imageUrl = img && urlFor(img).url(); + return ( +
+ {imageUrl && ( + My Image + )} +
+ ); + })} +
+
+ {post.authors?.map((author) => { + return ( + <> + + {' '} + and{' '} + + {author.name} + + ); + })} +
+
+
+
+ ); + })} +
+
+ + ); +} diff --git a/apps/frontpage/app/blog/layout.tsx b/apps/frontpage/app/blog/layout.tsx new file mode 100644 index 00000000..79a6306a --- /dev/null +++ b/apps/frontpage/app/blog/layout.tsx @@ -0,0 +1,41 @@ +import type { Metadata } from 'next'; +import { Header, Footer, Container } from '@repo/ui'; +import { fetchGithubCount } from '@repo/utils'; +import { generateDocsTree } from '../../lib/get-tree'; +import { Submenu } from '../../components/docs/submenu'; +import { docsVersions } from '@repo/utils'; + +export const metadata: Metadata = { + title: 'Storybook', + description: + "Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It's open source and free.", +}; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; + params: { slug: string[] }; +}) { + const { number: githubCount } = await fetchGithubCount(); + + const listofTrees = docsVersions.map((version) => { + return { + version: version.id, + tree: generateDocsTree(`content/docs/${version.id}`), + }; + }); + + return ( + <> +
} + variant="system" + /> + {children} +