From 0293bd84104de50ae0ce251fa4d24993a4a26ae4 Mon Sep 17 00:00:00 2001 From: Simon Vrachliotis Date: Fri, 25 Aug 2023 15:04:57 +1000 Subject: [PATCH 01/57] WIP - starting the port over from Astro site --- docs/.prettierrc.js | 6 + docs/keystatic.config.tsx | 61 +++++- docs/next.config.js | 3 + docs/src/app/(public)/docs/page.tsx | 2 +- .../app/(public)/showcase/[...slug]/page.tsx | 109 +++++++++++ docs/src/app/(public)/showcase/layout.tsx | 28 +++ docs/src/app/(public)/showcase/page.tsx | 52 ++++++ docs/src/components/navigation/header-nav.tsx | 11 +- .../projects/better-dev-screencasts.mdoc | 56 ++++++ docs/src/content/projects/blog-demo.mdoc | 173 ++++++++++++++++++ docs/src/content/projects/in-the-chair.mdoc | 11 ++ docs/src/content/projects/keystatic-docs.mdoc | 12 ++ docs/src/content/projects/luke-bennett.mdoc | 8 + .../projects/marketing-landing-page.mdoc | 71 +++++++ .../content/projects/react-sydney-meetup.mdoc | 10 + docs/src/content/projects/star-athletics.mdoc | 10 + docs/src/content/projects/sydjs-meetup.mdoc | 11 ++ docs/src/content/projects/thinkmill.mdoc | 10 + 18 files changed, 638 insertions(+), 6 deletions(-) create mode 100644 docs/.prettierrc.js create mode 100644 docs/src/app/(public)/showcase/[...slug]/page.tsx create mode 100644 docs/src/app/(public)/showcase/layout.tsx create mode 100644 docs/src/app/(public)/showcase/page.tsx create mode 100644 docs/src/content/projects/better-dev-screencasts.mdoc create mode 100644 docs/src/content/projects/blog-demo.mdoc create mode 100644 docs/src/content/projects/in-the-chair.mdoc create mode 100644 docs/src/content/projects/keystatic-docs.mdoc create mode 100644 docs/src/content/projects/luke-bennett.mdoc create mode 100644 docs/src/content/projects/marketing-landing-page.mdoc create mode 100644 docs/src/content/projects/react-sydney-meetup.mdoc create mode 100644 docs/src/content/projects/star-athletics.mdoc create mode 100644 docs/src/content/projects/sydjs-meetup.mdoc create mode 100644 docs/src/content/projects/thinkmill.mdoc diff --git a/docs/.prettierrc.js b/docs/.prettierrc.js new file mode 100644 index 000000000..84962a977 --- /dev/null +++ b/docs/.prettierrc.js @@ -0,0 +1,6 @@ +const defaultPrettierConfig = require('../.prettierrc.js'); + +module.exports = { + ...defaultPrettierConfig, + plugins: [...defaultPrettierConfig.plugins, 'prettier-plugin-tailwindcss'], +}; diff --git a/docs/keystatic.config.tsx b/docs/keystatic.config.tsx index a3a053bf6..3db685cc5 100644 --- a/docs/keystatic.config.tsx +++ b/docs/keystatic.config.tsx @@ -227,7 +227,8 @@ const markdocConfig: Config = { }, }; -const shouldUseCloudStorage = true; // process.env.NODE_ENV === 'production'; +// const shouldUseCloudStorage = true; // process.env.NODE_ENV === 'production'; +const shouldUseCloudStorage = process.env.NODE_ENV === 'production'; const pathPrefix = shouldUseCloudStorage ? 'docs/' : ''; export const readerPath = shouldUseCloudStorage ? process.cwd().replace(/\/docs/, '') @@ -357,6 +358,64 @@ export default config({ }, }), + // ------------------------------ + // Projects + // ------------------------------ + projects: collection({ + label: 'Projects (Showcase)', + slugField: 'title', + path: 'src/content/projects/*', + format: { contentField: 'content' }, + entryLayout: 'content', + schema: { + title: fields.slug({ name: { label: 'Title' } }), + type: fields.select({ + label: 'Type', + options: [ + { label: 'Production', value: 'production' }, + { label: 'Demo', value: 'demo' }, + ], + defaultValue: 'demo', + }), + url: fields.url({ label: 'URL' }), + repoUrl: fields.url({ + label: 'Repo URL', + description: + 'Fill this only for pulic repos, where it makes sense to share.', + }), + summary: fields.text({ + label: 'Summary', + multiline: true, + description: 'This will be used on the homepage listing.', + }), + // This will be replaced with a cloudImage field soon... + coverImage: fields.text({ label: 'Cover image' }), + sortIndex: fields.integer({ label: 'Sort Index', defaultValue: 100 }), + content: fields.document({ + label: 'Content', + description: + 'The long form copy for the project page. A link to a dedicated page will be available if this field is filled.', + formatting: true, + links: true, + // images: { + // directory: 'src/assets/projects', + // publicPath: '/images/projects', + // }, + + // This will be replaced with a cloudImage field soon... + componentBlocks: { + image: component({ + label: 'Image', + preview: () => null, + schema: { + image: fields.text({ label: 'Image' }), + }, + }), + }, + }), + }, + }), + // ------------------------------ // For testing purposes only // ------------------------------ diff --git a/docs/next.config.js b/docs/next.config.js index b34ff8cdc..5de75b753 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -5,6 +5,9 @@ module.exports = { experimental: { externalDir: true, }, + images: { + remotePatterns: [{ hostname: 'images.unsplash.com' }], + }, async redirects() { return [ { diff --git a/docs/src/app/(public)/docs/page.tsx b/docs/src/app/(public)/docs/page.tsx index d3f526337..6a2eca948 100644 --- a/docs/src/app/(public)/docs/page.tsx +++ b/docs/src/app/(public)/docs/page.tsx @@ -1,3 +1,3 @@ -export default function Docs() { +export default async function Docs() { return null; } diff --git a/docs/src/app/(public)/showcase/[...slug]/page.tsx b/docs/src/app/(public)/showcase/[...slug]/page.tsx new file mode 100644 index 000000000..01de5b43e --- /dev/null +++ b/docs/src/app/(public)/showcase/[...slug]/page.tsx @@ -0,0 +1,109 @@ +import { notFound } from 'next/navigation'; +import { Metadata, ResolvingMetadata } from 'next'; +import slugify from '@sindresorhus/slugify'; + +import { TableOfContents } from '../../../../components/navigation/table-of-contents'; +import DocumentRenderer from '../../../../components/document-renderer'; +import { reader } from '../../../../utils/reader'; +import { H1_ID } from '../../../../constants'; + +type DocsProps = { + params: { slug: string[] }; +}; + +export default async function Docs({ params }: DocsProps) { + const { slug: slugPath } = params; + + const slug = slugPath.join('/'); + const page = await reader.collections.pages.read(slug); + + if (!page) notFound(); + + // Filter headings from the document content + const headingsFromContent = (await page.content()) + .filter(child => child.type === 'heading') + .map(heading => ({ + level: heading.level as number, + text: heading.children.find(child => child.text)?.text as string, + })) + .filter(heading => heading.text); + + // Add slug + const headingsWithSlugs = headingsFromContent.map(({ level, text }) => { + return { + level, + text, + slug: `#${slugify(text)}`, + }; + }); + + // Manually add the persistent #${H1_ID} heading, to send to TOCs + const overviewHeading = { + level: 1, + text: 'Overview', + slug: `#${H1_ID}`, + }; + const headings = [overviewHeading, ...headingsWithSlugs]; + + return ( +
+
+

+ {page.title} +

+
+ +
+
+ +
+ ); +} + +export async function generateStaticParams() { + const slugs = await reader.collections.pages.list(); + + return slugs.map(slug => ({ + slug: slug.split('/'), + })); +} + +export async function generateMetadata( + { params }: DocsProps, + parent: ResolvingMetadata +): Promise { + const slugPath = params.slug; + const slug = slugPath.join('/'); + + const page = await reader.collections.pages.read(slug); + + const parentTitle = (await parent).title ?? 'Docs'; + const title = page?.title ?? parentTitle; + + const fallbackDescription = 'Documentation page for Keystatic.'; + const description = page?.summary ? page.summary : fallbackDescription; + + const image = `/og?title=${title}`; + const parentTwitterSite = (await parent).twitter?.site ?? ''; + + return { + title, + description, + openGraph: { + title, + description, + url: `https://keystatic.com/docs/${slug}`, + type: 'website', + images: [{ url: image }], + }, + twitter: { + card: 'summary_large_image', + title, + description, + site: parentTwitterSite, + }, + }; +} diff --git a/docs/src/app/(public)/showcase/layout.tsx b/docs/src/app/(public)/showcase/layout.tsx new file mode 100644 index 000000000..8d9a26273 --- /dev/null +++ b/docs/src/app/(public)/showcase/layout.tsx @@ -0,0 +1,28 @@ +import { Main } from '../../../components/main'; +import { DocsFooter } from '../../../components/footer'; + +export const metadata = { + title: { + template: '%s - Showcase | Keystatic', + default: 'Showcase', + }, +}; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {/** CONTENT */} + +
+ {/** INNER CONTENT */} +
{children}
+
+ + +
+ ); +} diff --git a/docs/src/app/(public)/showcase/page.tsx b/docs/src/app/(public)/showcase/page.tsx new file mode 100644 index 000000000..90efdd74b --- /dev/null +++ b/docs/src/app/(public)/showcase/page.tsx @@ -0,0 +1,52 @@ +import Image from 'next/image'; +import { reader } from '../../../utils/reader'; + +export default async function Showcase() { + const projects = await reader.collections.projects.all(); + + console.log({ projects }); + const sortedProjects = projects.sort((a, b) => { + return (a.entry.sortIndex = b.entry.sortIndex); + }); + + return ( +
+
+

Built with Keystatic

+

+ A collection of projects using Keystatic to manage parts of their + codebase. +

+
+ +
    + {sortedProjects.map(async ({ slug, entry }) => { + return ( +
  • +
    +
    + +
    +
    +
    +

    + {entry.title} +

    + {/* */} +
    +

    {entry.summary}

    +
    + {/* */} +
    +
  • + ); + })} +
+
+ ); +} diff --git a/docs/src/components/navigation/header-nav.tsx b/docs/src/components/navigation/header-nav.tsx index 0f7bf839c..54e3f87f4 100644 --- a/docs/src/components/navigation/header-nav.tsx +++ b/docs/src/components/navigation/header-nav.tsx @@ -73,10 +73,13 @@ export function HeaderNav({ Blog Showcase diff --git a/docs/src/content/projects/better-dev-screencasts.mdoc b/docs/src/content/projects/better-dev-screencasts.mdoc new file mode 100644 index 000000000..0827f67ba --- /dev/null +++ b/docs/src/content/projects/better-dev-screencasts.mdoc @@ -0,0 +1,56 @@ +--- +title: Better Dev Screencasts +type: production +url: https://betterdevscreencasts.com +repoUrl: https://github.com/simonswiss/better-dev +summary: >- + A collection of resources to help developers create better screencasts, and + level-up their overall video content creation game. +coverImage: >- + https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 50 +--- +Better Dev Screencasts was possibly the **very first Keystatic website in production**. In early 2023, Simon from the Thinkmill Labs team used this site to "dogfood" Keystatic as it was being developed. + +To this date, Keystatic powers the site's `posts` content types, which support custom components for YouTube videos and Tweet embeds. + +Missing image ../../assets/projects/better-dev-screencasts/admin-ui-screenshot.png + +## Keystatic config + +Here's what the config for the `posts` collection looks like in this project: + +```typescript +posts: collection({ + label: 'Posts', + path: 'content/posts/*', + slugField: 'title', + format: { + contentField: 'content', + }, + entryLayout: 'content', + schema: { + title: fields.slug({ name: { label: 'Title' } }), + date: fields.date({ + label: 'Date', + validation: { isRequired: true } + }), + excerpt: fields.text({ label: 'Excerpt' }), + coverImage: fields.image({ + label: 'Cover image', + directory: 'public/images/posts', + publicPath: '/images/posts', + }), + content: fields.document({ + label: 'Post copy', + layouts: [[1, 1]], + formatting: true, + dividers: true, + links: true, + componentBlocks, + }), + }, +}), +``` + +The API for the `componentBlocks` is slightly outdated and likely to change in the future, so we won't share the code snippet here — but you can find it on the project's [GitHub repo](https://github.com/simonswiss/better-dev/blob/main/component-blocks.tsx) which is public! diff --git a/docs/src/content/projects/blog-demo.mdoc b/docs/src/content/projects/blog-demo.mdoc new file mode 100644 index 000000000..bd9605193 --- /dev/null +++ b/docs/src/content/projects/blog-demo.mdoc @@ -0,0 +1,173 @@ +--- +title: Blog Demo +type: demo +url: https://keystatic-demo-blog.vercel.app/ +repoUrl: https://github.com/Thinkmill/keystatic-demo-blog +summary: A basic blog templatebuilt with Keystatic, Next.js and Tailwind CSS. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 110 +--- +This blog demo is a basic implementation of a Keystatic schema to power a blog. + +## Collections and Singletons + +Multiple `collections` are defined in this project: `posts`, `external-articles` and `authors`. + +There are also two `singletons` defined to manage content for the `homepage` and the `about` page. + +![Screenshot of the Keystatic Admin UI](../../assets/projects/blog-demo/admin-ui.png) + +## Keystatic config + +Here's what the config file looks like for this project: + +```typescript +import { + collection, + config, + fields, + singleton, +} from "@keystatic/core"; +import { ComponentBlocks } from "./components/ComponentBlocks"; + +export default config({ + storage: { kind: "local" }, + singletons: { + home: singleton({ + label: "Home", + path: "content/pages/home/", + schema: { + heading: fields.document({ + formatting: { + inlineMarks: { + bold: true, + }, + }, + label: "Heading (note: text that is bolded will show up in red)", + }), + }, + }), + about: singleton({ + label: "About", + path: "content/pages/about/", + schema: { + content: fields.document({ + formatting: true, + dividers: true, + links: true, + layouts: [ + [1, 1], + [1, 1, 1], + [2, 1], + [1, 2, 1], + ], + label: "Content", + componentBlocks: ComponentBlocks, + }), + }, + }), + }, + collections: { + authors: collection({ + label: "Authors", + path: "content/authors/*", + slugField: "name", + schema: { + name: fields.slug({ + name: { + label: "Name", + validation: { + length: { + min: 1, + }, + }, + }, + }), + role: fields.text({ label: "Role" }), + avatar: fields.image({ + label: "Author avatar", + directory: "public/images/authors", + }), + }, + }), + posts: collection({ + label: "Posts", + path: "content/posts/*/", + slugField: "title", + schema: { + title: fields.slug({ + name: { + label: "Title", + }, + }), + summary: fields.text({ + label: "Summary", + validation: { length: { min: 4 } }, + }), + publishedDate: fields.date({ label: "Published Date" }), + coverImage: fields.image({ + label: "Image", + directory: "public/images/posts", + }), + wordCount: fields.integer({ + label: "Word count", + }), + authors: fields.array( + fields.relationship({ + label: "Post author", + collection: "authors", + }), + { + label: "Authors", + validation: { length: { min: 1 } }, + itemLabel: (props) => props.value || "Please select an author", + } + ), + content: fields.document({ + formatting: true, + dividers: true, + links: true, + layouts: [ + [1, 1], + [1, 1, 1], + [2, 1], + [1, 2, 1], + ], + label: "Content", + componentBlocks: ComponentBlocks, + }), + }, + }), + externalArticles: collection({ + label: "External Article", + path: "content/externalArticles/*/", + slugField: "title", + schema: { + title: fields.slug({ + name: { + label: "Title", + validation: { length: { min: 4 } }, + }, + }), + directLink: fields.url({ + label: "Article Link", + }), + source: fields.text({ + label: "Link Source", + defaultValue: "Read more.", + }), + coverImage: fields.image({ + label: "Cover Image", + directory: "public/images/external-articles", + }), + summary: fields.text({ + label: "Summary", + validation: { length: { min: 4, max: 200 } }, + }), + publishedDate: fields.date({ label: "Published Date" }), + }, + }), + }, +}); + +``` diff --git a/docs/src/content/projects/in-the-chair.mdoc b/docs/src/content/projects/in-the-chair.mdoc new file mode 100644 index 000000000..cfa049e94 --- /dev/null +++ b/docs/src/content/projects/in-the-chair.mdoc @@ -0,0 +1,11 @@ +--- +title: In The Chair +type: production +url: https://inthechair.com +summary: >- + Take bookings and payments 24/7, automate reminders and keep your clients + coming back with IN THE CHAIR. Get your all-in-one barber appointment app FREE + now. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 60 +--- diff --git a/docs/src/content/projects/keystatic-docs.mdoc b/docs/src/content/projects/keystatic-docs.mdoc new file mode 100644 index 000000000..378b7488a --- /dev/null +++ b/docs/src/content/projects/keystatic-docs.mdoc @@ -0,0 +1,12 @@ +--- +title: Keystatic +type: production +url: https://keystatic.com +repoUrl: https://github.com/Thinkmill/keystatic/tree/main/docs +summary: >- + Keystatic is a new tool from Thinkmill Labs that opens up your code-based + content (written in Markdown, JSON or YAML) to contributors who aren't + technical. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 40 +--- diff --git a/docs/src/content/projects/luke-bennett.mdoc b/docs/src/content/projects/luke-bennett.mdoc new file mode 100644 index 000000000..a271461ff --- /dev/null +++ b/docs/src/content/projects/luke-bennett.mdoc @@ -0,0 +1,8 @@ +--- +title: Luke Bennett +type: production +url: https://lukebennett.com.au +summary: Luke Bennett's personal website. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 30 +--- diff --git a/docs/src/content/projects/marketing-landing-page.mdoc b/docs/src/content/projects/marketing-landing-page.mdoc new file mode 100644 index 000000000..6cad25b5d --- /dev/null +++ b/docs/src/content/projects/marketing-landing-page.mdoc @@ -0,0 +1,71 @@ +--- +title: Marketing Landing Page +type: demo +url: https://keystatic-demo-landing-page.vercel.app +repoUrl: https://github.com/Thinkmill/keystatic-demo-landing-page +summary: >- + A marketing landing page demo site, built with Keystatic, Next.js and Tailwind + CSS +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 100 +--- +The Marketing Landing Page demo was one of the first projects powered by Keystatic. + +Before the [Keystatic CLI](https://keystatic.com/docs/quick-start) was a thing (and became the recommended way to start new Keystatic projects), this was one of the "starter templates" for newcomers to Keystatic. + +## Singletons and Collections + +This templates demonstrates the use of both Keystatic content types: `singletons` and `collections`: + +![Keystatic dashboard with singleton and collection content types](../../assets/projects/marketing-landing-page/dashboard.png) + +## Keystatic config + +Here's what the config file looks like: + +```typescript +import { + collection, + config, + fields, + singleton, +} from "@keystatic/core"; + +export default config({ + storage: { kind: "local" }, + singletons: { + landingPage: singleton({ + label: "Landing Page", + path: "content/landing-page/", + schema: { + heroHeadline: fields.text({ label: "Hero headline" }), + heroIntroText: fields.text({ + label: "Hero intro text", + multiline: true, + }), + footerHeadline: fields.text({ label: "Footer headline" }), + footerText: fields.text({ label: "Footer text", multiline: true }), + }, + }), + }, + collections: { + testimonials: collection({ + path: "content/testimonials/*/", + label: "Testimonials", + slugField: "author", + schema: { + author: fields.slug({ name: { label: "Author" } }), + testimonial: fields.text({ label: "Testimonial", multiline: true }), + featured: fields.checkbox({ label: "Featured testimonial" }), + twitterHandle: fields.text({ label: "Twitter handle" }), + avatar: fields.image({ + label: "Avatar", + directory: "public/images/testimonials", + validation: { isRequired: true }, + }), + }, + }), + }, +}); + +``` diff --git a/docs/src/content/projects/react-sydney-meetup.mdoc b/docs/src/content/projects/react-sydney-meetup.mdoc new file mode 100644 index 000000000..0d02f496b --- /dev/null +++ b/docs/src/content/projects/react-sydney-meetup.mdoc @@ -0,0 +1,10 @@ +--- +title: React Sydney Meetup +type: production +url: https://react.sydney +summary: >- + Join the vibrant and inclusive community of web developers discussing the + latest in the React space from Sydney, Australia. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 70 +--- diff --git a/docs/src/content/projects/star-athletics.mdoc b/docs/src/content/projects/star-athletics.mdoc new file mode 100644 index 000000000..d60c38fdb --- /dev/null +++ b/docs/src/content/projects/star-athletics.mdoc @@ -0,0 +1,10 @@ +--- +title: Star Athletics +type: production +url: https://star-athletics.com.au +summary: >- + Fun-filled athletics in Sydney’s Northern Beaches and Woolgoolga on the + Mid-North Coast. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 10 +--- diff --git a/docs/src/content/projects/sydjs-meetup.mdoc b/docs/src/content/projects/sydjs-meetup.mdoc new file mode 100644 index 000000000..7565b3253 --- /dev/null +++ b/docs/src/content/projects/sydjs-meetup.mdoc @@ -0,0 +1,11 @@ +--- +title: SydJS Meetup +type: production +url: https://sydjs.com +repoUrl: https://github.com/Thinkmill/sydjs-keystatic +summary: >- + Join the vibrant and inclusive community of web developers discussing the + latest in Javascript from Sydney, Australia. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 20 +--- diff --git a/docs/src/content/projects/thinkmill.mdoc b/docs/src/content/projects/thinkmill.mdoc new file mode 100644 index 000000000..93642cd14 --- /dev/null +++ b/docs/src/content/projects/thinkmill.mdoc @@ -0,0 +1,10 @@ +--- +title: Thinkmill +type: production +url: https://thinkmill.com.au +summary: >- + An internationally recognised design & development consultancy. We help + ambitious teams make software that matters. +coverImage: https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjJ8fGJsdXIlMjBhYnN0cmFjdHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60 +sortIndex: 0 +--- From 8ec60e76167e1dcd3e2ea037984684dc68678b24 Mon Sep 17 00:00:00 2001 From: Simon Vrachliotis Date: Fri, 25 Aug 2023 15:24:23 +1000 Subject: [PATCH 02/57] Sorting order fixed and width container removal --- docs/src/app/(public)/showcase/page.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/src/app/(public)/showcase/page.tsx b/docs/src/app/(public)/showcase/page.tsx index 90efdd74b..30c6e73d2 100644 --- a/docs/src/app/(public)/showcase/page.tsx +++ b/docs/src/app/(public)/showcase/page.tsx @@ -1,24 +1,26 @@ import Image from 'next/image'; +import { notFound } from 'next/navigation'; + import { reader } from '../../../utils/reader'; export default async function Showcase() { const projects = await reader.collections.projects.all(); - console.log({ projects }); + if (!projects) notFound(); + const sortedProjects = projects.sort((a, b) => { - return (a.entry.sortIndex = b.entry.sortIndex); + return (a.entry.sortIndex as number) - (b.entry.sortIndex as number); }); return ( -
-
+ <> +

Built with Keystatic

A collection of projects using Keystatic to manage parts of their codebase.

-
    {sortedProjects.map(async ({ slug, entry }) => { return ( @@ -47,6 +49,6 @@ export default async function Showcase() { ); })}
-
+ ); } From 4246373563c6473530d841371230241df8563091 Mon Sep 17 00:00:00 2001 From: Simon Vrachliotis Date: Fri, 25 Aug 2023 16:58:56 +1000 Subject: [PATCH 03/57] =?UTF-8?q?WIP=20continued=20=E2=80=94=C2=A0overall?= =?UTF-8?q?=20layout=20shaping=20up=20(not=20responsive!)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/package.json | 1 + docs/src/app/(public)/showcase/page.tsx | 127 ++++++++++++++++++------ docs/tailwind.config.js | 1 + pnpm-lock.yaml | 10 ++ 4 files changed, 108 insertions(+), 31 deletions(-) diff --git a/docs/package.json b/docs/package.json index 92b6615da..2dfeb2ac7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,6 +19,7 @@ "@keystatic/next": "^1.0.0", "@markdoc/markdoc": "^0.3.0", "@sindresorhus/slugify": "^1.1.2", + "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.3", "@types/cookie": "^0.5.1", "@types/node": "16.11.13", diff --git a/docs/src/app/(public)/showcase/page.tsx b/docs/src/app/(public)/showcase/page.tsx index 30c6e73d2..2374d5880 100644 --- a/docs/src/app/(public)/showcase/page.tsx +++ b/docs/src/app/(public)/showcase/page.tsx @@ -1,10 +1,19 @@ import Image from 'next/image'; +import Link from 'next/link'; import { notFound } from 'next/navigation'; +import type { Entry } from '@keystatic/core/reader'; import { reader } from '../../../utils/reader'; +import keystaticConfig from '../../../../keystatic.config'; +import Button from '../../../components/button'; + +type Project = { + slug: string; + entry: Entry<(typeof keystaticConfig)['collections']['projects']>; +}; export default async function Showcase() { - const projects = await reader.collections.projects.all(); + const projects: Project[] = await reader.collections.projects.all(); if (!projects) notFound(); @@ -12,43 +21,99 @@ export default async function Showcase() { return (a.entry.sortIndex as number) - (b.entry.sortIndex as number); }); + const highlightedProjects = sortedProjects.splice(0, 2); + return ( <> -
+

Built with Keystatic

-

- A collection of projects using Keystatic to manage parts of their - codebase. -

+
+

+ A collection of projects using Keystatic to manage parts of their + codebase. +

+

+ Built something with Keystatic?{' '} + + Share your project + +

+
-
    + + {/* Highlighted projects */} +
      + {highlightedProjects.map(async ({ slug, entry }) => { + return ; + })} +
    + +
      {sortedProjects.map(async ({ slug, entry }) => { - return ( -
    • -
      -
      - -
      -
      -
      -

      - {entry.title} -

      - {/* */} -
      -

      {entry.summary}

      -
      - {/* */} -
      -
    • - ); + return ; })}
    ); } + +function ProjectCard({ entry, slug }: Project) { + return ( +
  • +
    +
    +

    + {entry.title} +

    + {entry.type === 'demo' && } +
    +

    {entry.summary}

    +
    + {/* TODO: New button styles */} + +
    +
    + +
    + +
    +
  • + ); +} + +function DemoBadge() { + return ( + + + + + Demo + + ); +} diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js index 659467997..1d38c7257 100644 --- a/docs/tailwind.config.js +++ b/docs/tailwind.config.js @@ -48,5 +48,6 @@ module.exports = { require('@tailwindcss/forms')({ strategy: 'class', // only generate classes (to not globally override Voussiour input styles) }), + require('@tailwindcss/container-queries'), ], }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f4d2b1be..cc3ad6375 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -587,6 +587,7 @@ importers: '@keystatic/next': ^1.0.0 '@markdoc/markdoc': ^0.3.0 '@sindresorhus/slugify': ^1.1.2 + '@tailwindcss/container-queries': ^0.1.1 '@tailwindcss/forms': ^0.5.3 '@tailwindcss/typography': ^0.5.9 '@types/cookie': ^0.5.1 @@ -615,6 +616,7 @@ importers: '@keystatic/next': link:../packages/next '@markdoc/markdoc': 0.3.0_i2bxsyfskhzbpjanbovidbfj7u '@sindresorhus/slugify': 1.1.2 + '@tailwindcss/container-queries': 0.1.1_tailwindcss@3.3.3 '@tailwindcss/forms': 0.5.3_tailwindcss@3.3.3 '@types/cookie': 0.5.1 '@types/node': 16.11.13 @@ -8929,6 +8931,14 @@ packages: defer-to-connect: 2.0.1 dev: true + /@tailwindcss/container-queries/0.1.1_tailwindcss@3.3.3: + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} + peerDependencies: + tailwindcss: '>=3.2.0' + dependencies: + tailwindcss: 3.3.3 + dev: false + /@tailwindcss/forms/0.5.3_tailwindcss@3.3.3: resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==} peerDependencies: From fb5a176a83806f8957a9649026c47437dbf18243 Mon Sep 17 00:00:00 2001 From: Simon Vrachliotis Date: Mon, 28 Aug 2023 12:39:09 +1000 Subject: [PATCH 04/57] ActionButton WIP --- docs/.prettierrc.js | 5 +- docs/src/app/(public)/showcase/page.tsx | 20 ++++--- docs/src/components/action-button.tsx | 53 +++++++++++++++++++ .../components/icons/arrow-top-right-icon.tsx | 32 +++++++++++ .../components/icons/github-outline-icon.tsx | 26 +++++++++ docs/src/components/icons/globe.tsx | 42 +++++++++++++++ 6 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 docs/src/components/action-button.tsx create mode 100644 docs/src/components/icons/arrow-top-right-icon.tsx create mode 100644 docs/src/components/icons/github-outline-icon.tsx create mode 100644 docs/src/components/icons/globe.tsx diff --git a/docs/.prettierrc.js b/docs/.prettierrc.js index 84962a977..a1ce37b04 100644 --- a/docs/.prettierrc.js +++ b/docs/.prettierrc.js @@ -2,5 +2,8 @@ const defaultPrettierConfig = require('../.prettierrc.js'); module.exports = { ...defaultPrettierConfig, - plugins: [...defaultPrettierConfig.plugins, 'prettier-plugin-tailwindcss'], + plugins: [ + ...(defaultPrettierConfig.plugins || []), + 'prettier-plugin-tailwindcss', + ], }; diff --git a/docs/src/app/(public)/showcase/page.tsx b/docs/src/app/(public)/showcase/page.tsx index 2374d5880..49e02ea8b 100644 --- a/docs/src/app/(public)/showcase/page.tsx +++ b/docs/src/app/(public)/showcase/page.tsx @@ -5,7 +5,9 @@ import type { Entry } from '@keystatic/core/reader'; import { reader } from '../../../utils/reader'; import keystaticConfig from '../../../../keystatic.config'; -import Button from '../../../components/button'; +import ActionButton from '../../../components/action-button'; +import { GlobeIcon } from '../../../components/icons/globe'; +import { GitHubOutlineIcon } from '../../../components/icons/github-outline-icon'; type Project = { slug: string; @@ -60,7 +62,7 @@ export default async function Showcase() { ); } -function ProjectCard({ entry, slug }: Project) { +function ProjectCard({ entry }: Project) { return (
  • @@ -73,14 +75,16 @@ function ProjectCard({ entry, slug }: Project) {

    {entry.summary}

    {/* TODO: New button styles */} - + View on GitHub +
    diff --git a/docs/src/components/action-button.tsx b/docs/src/components/action-button.tsx new file mode 100644 index 000000000..ac1ed7cd1 --- /dev/null +++ b/docs/src/components/action-button.tsx @@ -0,0 +1,53 @@ +import Link from 'next/link'; +import { AllHTMLAttributes, ButtonHTMLAttributes } from 'react'; + +import { cx } from '../utils'; +import { GlobeIcon } from './icons/globe'; +import { ArrowTopRightIcon } from './icons/arrow-top-right-icon'; + +type ButtonProps = { + impact?: 'bold' | 'light'; + href?: string; + isLoading?: boolean; + icon?: typeof GlobeIcon; + type?: ButtonHTMLAttributes['type']; +} & AllHTMLAttributes; + +const baseClasses = + 'flex rounded-md px-3 py-2 text-center text-sm text-slate-900 font-medium leading-none border transition-colors gap-1 items-center'; + +const impactClasses: Record = { + bold: 'bg-white border-slate-200 text-black hover:bg-slate-50', + light: 'bg-transparent border-transparent text-black hover:border-slate-200', +}; + +// ---------- + +export default function ActionButton({ + impact = 'bold', + href, + children, + icon: Icon, + className = '', + ...props +}: ButtonProps) { + return href ? ( + + {Icon && } + {children} + {href.startsWith('http') && } + + ) : ( + + ); +} diff --git a/docs/src/components/icons/arrow-top-right-icon.tsx b/docs/src/components/icons/arrow-top-right-icon.tsx new file mode 100644 index 000000000..5dad98522 --- /dev/null +++ b/docs/src/components/icons/arrow-top-right-icon.tsx @@ -0,0 +1,32 @@ +export type IconProps = { + ariaHidden?: boolean; +} & React.SVGProps; + +export function ArrowTopRightIcon({ + ariaHidden = true, + ...restProps +}: IconProps) { + return ( + + + + + ); +} diff --git a/docs/src/components/icons/github-outline-icon.tsx b/docs/src/components/icons/github-outline-icon.tsx new file mode 100644 index 000000000..605a7f324 --- /dev/null +++ b/docs/src/components/icons/github-outline-icon.tsx @@ -0,0 +1,26 @@ +export type IconProps = { + ariaHidden?: boolean; +} & React.SVGProps; + +export function GitHubOutlineIcon({ + ariaHidden = true, + ...restProps +}: IconProps) { + return ( + + + + ); +} diff --git a/docs/src/components/icons/globe.tsx b/docs/src/components/icons/globe.tsx new file mode 100644 index 000000000..5280f6543 --- /dev/null +++ b/docs/src/components/icons/globe.tsx @@ -0,0 +1,42 @@ +export type IconProps = { + ariaHidden?: boolean; +} & React.SVGProps; + +export function GlobeIcon({ ariaHidden = true, ...restProps }: IconProps) { + return ( + + + + + + + + + + + + + ); +} From 1878da12905decbb9815f29f89735be818e597ca Mon Sep 17 00:00:00 2001 From: Simon Vrachliotis Date: Mon, 28 Aug 2023 12:55:48 +1000 Subject: [PATCH 05/57] Navigation --- docs/.vscode/extensions.json | 4 ++++ docs/.vscode/settings.json | 19 ++++++++++++++++++- docs/src/components/navigation/header-nav.tsx | 15 ++++++++------- .../navigation/keystatic-logo-link.tsx | 19 ++++++++++++++++++- docs/src/components/navigation/nav-item.tsx | 4 ++-- 5 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 docs/.vscode/extensions.json diff --git a/docs/.vscode/extensions.json b/docs/.vscode/extensions.json new file mode 100644 index 000000000..4ea9f1e83 --- /dev/null +++ b/docs/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["cpylua.language-postcss", "bradlc.vscode-tailwindcss"], + "unwantedRecommendations": [] +} diff --git a/docs/.vscode/settings.json b/docs/.vscode/settings.json index 37ed20c52..abc4119d3 100644 --- a/docs/.vscode/settings.json +++ b/docs/.vscode/settings.json @@ -1,4 +1,21 @@ { "typescript.tsdk": "../node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + // Tailwind Intellisense power-up + // Extend scope of supported attributes with `{anything}Classes`: + "tailwindCSS.classAttributes": [ + "class", + "className", + "ngClass", + ".*Classes.*" + ], + // Regex black magic: + "tailwindCSS.experimental.classRegex": [ + // tailwind-variants + ["tv\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + // CVA (with support for JSON) + ["cva(s*(\"[^\"]*\"|s*,s*|{(?:[^{}]|(?:{(?:[^{}]|(?:{[^{}]*}))*}))*}s*)*)"], + // `cx()` function from classnames / clsx + ["cx\\(([^)]*)\\)", "(?:\"|'|`)([^(?:\"|'|`)]*)(?:\"|'|`)"] + ] } diff --git a/docs/src/components/navigation/header-nav.tsx b/docs/src/components/navigation/header-nav.tsx index 54e3f87f4..3ca9e3bad 100644 --- a/docs/src/components/navigation/header-nav.tsx +++ b/docs/src/components/navigation/header-nav.tsx @@ -30,26 +30,27 @@ export function HeaderNav({ const isDocsNav = pathname?.startsWith('/docs') && !ignoreDocNavStyles; const linkStylesShared = - 'shrink-0 px-3 rounded-lg transition-colors h-10 flex items-center font-semibold relative'; + 'shrink-0 px-4 rounded-md transition-colors h-10 flex items-center relative'; const linkStylesIdle = - 'hover:bg-keystatic-gray-light active:bg-keystatic-gray'; + 'hover:bg-keystatic-gray-light active:bg-keystatic-gray font-medium'; - const linkStylesCurrent = 'bg-keystatic-gray hover:bg-keystatic-gray'; + const linkStylesCurrent = + 'bg-keystatic-gray hover:bg-keystatic-gray font-bold'; return (
    -