diff --git a/apps/studio/prisma/generated/generatedEnums.ts b/apps/studio/prisma/generated/generatedEnums.ts index 616f4528ba..1e9d831e34 100644 --- a/apps/studio/prisma/generated/generatedEnums.ts +++ b/apps/studio/prisma/generated/generatedEnums.ts @@ -8,6 +8,7 @@ export const ResourceType = { Page: "Page", Folder: "Folder", Collection: "Collection", + CollectionMeta: "CollectionMeta", CollectionLink: "CollectionLink", CollectionPage: "CollectionPage", IndexPage: "IndexPage", diff --git a/apps/studio/prisma/migrations/20241217054133_add_collection_meta/migration.sql b/apps/studio/prisma/migrations/20241217054133_add_collection_meta/migration.sql new file mode 100644 index 0000000000..2d984b5817 --- /dev/null +++ b/apps/studio/prisma/migrations/20241217054133_add_collection_meta/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ResourceType" ADD VALUE 'CollectionMeta'; diff --git a/apps/studio/prisma/schema.prisma b/apps/studio/prisma/schema.prisma index 36521d4a61..47180f971c 100644 --- a/apps/studio/prisma/schema.prisma +++ b/apps/studio/prisma/schema.prisma @@ -77,7 +77,11 @@ model Resource { // This is required so prisma does not attempt to drop the custom index. @@unique([siteId, parentId, permalink]) @@index([siteId, id, parentId]) // note: ordering is important here! - @@index([type]) + @@index([type]) + + // NOTE: This is used to create a inverted index using text trigrams for the title + // so that we can perform searches on the title quickly + @@index([title(ops: raw("gin_trgm_ops"))], type: Gin, name: "resource_title_trgm_idx") } model Blob { @@ -103,6 +107,7 @@ enum ResourceType { Page Folder Collection + CollectionMeta // Can only ever be inside collection CollectionLink // Can only ever be inside collection CollectionPage // Can only live inside `Collection` resources IndexPage // This denotes the index page of a folder or a collection diff --git a/apps/studio/prisma/types.ts b/apps/studio/prisma/types.ts index 801a707175..04fdba7115 100644 --- a/apps/studio/prisma/types.ts +++ b/apps/studio/prisma/types.ts @@ -6,6 +6,7 @@ */ import type { + IsomerLayoutVariants as _IsomerLayoutVariants, IsomerPageSchemaType as _IsomerPageSchemaType, IsomerSchema as _IsomerSchema, IsomerSiteConfigProps as _IsomerSiteConfigProps, @@ -20,6 +21,10 @@ declare global { // TODO: Rename all with XXXYYYJson instead of XXXJsonYYY type SiteJsonConfig = Tagged<_IsomerSiteConfigProps, "JSONB"> type SiteThemeJson = Tagged<_IsomerSiteThemeProps, "JSONB"> + type CollectionThemeJson = Tagged< + _IsomerLayoutVariants["collection"], + "JSONB" + > type BlobJsonContent = Tagged<_IsomerSchema, "JSONB"> type NavbarJsonContent = Tagged< _IsomerSiteWideComponentsProps["navBarItems"], diff --git a/apps/studio/public/assets/css/preview-tw.css b/apps/studio/public/assets/css/preview-tw.css index f1e2c9b213..ca673a2f7d 100644 --- a/apps/studio/public/assets/css/preview-tw.css +++ b/apps/studio/public/assets/css/preview-tw.css @@ -1249,6 +1249,10 @@ video { margin-right: -0.5rem; } +.-mt-1 { + margin-top: -0.25rem; +} + .-mt-8 { margin-top: -2rem; } @@ -2540,6 +2544,11 @@ video { padding-right: 0.25rem; } +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} + .px-10 { padding-left: 2.5rem; padding-right: 2.5rem; @@ -2590,6 +2599,16 @@ video { padding-right: 1px; } +.py-0 { + padding-top: 0px; + padding-bottom: 0px; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; @@ -4696,6 +4715,10 @@ video { justify-content: flex-start; } + .sm\:gap-0 { + gap: 0px; + } + .sm\:gap-3 { gap: 0.75rem; } @@ -4891,6 +4914,10 @@ video { gap: 1rem; } + .md\:gap-5 { + gap: 1.25rem; + } + .md\:gap-7 { gap: 1.75rem; } @@ -4946,6 +4973,10 @@ video { padding-bottom: 0.75rem; } + .md\:pt-0 { + padding-top: 0px; + } + .md\:pt-16 { padding-top: 4rem; } diff --git a/apps/studio/src/components/AuthWrappers/PermissionsBoundary.tsx b/apps/studio/src/components/AuthWrappers/PermissionsBoundary.tsx index 77ce1744cc..2cd8732c31 100644 --- a/apps/studio/src/components/AuthWrappers/PermissionsBoundary.tsx +++ b/apps/studio/src/components/AuthWrappers/PermissionsBoundary.tsx @@ -20,6 +20,12 @@ const ERROR_COMPONENT_PROPS: Record = { "To have access, ask your site admins to assign this collection to you", buttonText: "Back to My Sites", }, + CollectionMeta: { + title: "You don't have access to edit this collection.", + description: + "To have access, ask your site admins to assign this collection to you", + buttonText: "Back to My Sites", + }, IndexPage: { title: "You don't have access to edit this page.", description: diff --git a/apps/studio/src/components/PageEditor/ComponentSelector.tsx b/apps/studio/src/components/PageEditor/ComponentSelector.tsx index b51d768109..65083eb8e1 100644 --- a/apps/studio/src/components/PageEditor/ComponentSelector.tsx +++ b/apps/studio/src/components/PageEditor/ComponentSelector.tsx @@ -182,6 +182,7 @@ function ComponentSelector() { return [] case ResourceType.Folder: case ResourceType.FolderMeta: + case ResourceType.CollectionMeta: throw new Error(`Unsupported resource type: ${type}`) default: const exhaustiveCheck: never = type diff --git a/apps/studio/src/features/dashboard/components/DirectorySidebar/constants.ts b/apps/studio/src/features/dashboard/components/DirectorySidebar/constants.ts index c93cb0819f..ba43c6d8ae 100644 --- a/apps/studio/src/features/dashboard/components/DirectorySidebar/constants.ts +++ b/apps/studio/src/features/dashboard/components/DirectorySidebar/constants.ts @@ -1,6 +1,7 @@ import type { IconType } from "react-icons" import { ResourceType } from "~prisma/generated/generatedEnums" import { + BiCog, BiData, BiFile, BiFolder, @@ -14,6 +15,7 @@ export const ICON_MAPPINGS: Record = { [ResourceType.Folder]: BiFolder, [ResourceType.Collection]: BiData, [ResourceType.CollectionPage]: BiFile, + [ResourceType.CollectionMeta]: BiCog, [ResourceType.CollectionLink]: BiLink, [ResourceType.RootPage]: BiHomeAlt, [ResourceType.IndexPage]: BiFile, diff --git a/apps/studio/src/features/dashboard/components/DirectorySidebar/useIsActive.ts b/apps/studio/src/features/dashboard/components/DirectorySidebar/useIsActive.ts index 7c64446808..58586af1f3 100644 --- a/apps/studio/src/features/dashboard/components/DirectorySidebar/useIsActive.ts +++ b/apps/studio/src/features/dashboard/components/DirectorySidebar/useIsActive.ts @@ -34,6 +34,7 @@ export const useIsActive = ( case ResourceType.CollectionLink: return siteProps.linkId === currentResourceId case ResourceType.FolderMeta: + case ResourceType.CollectionMeta: // TODO: Not implemented yet return false default: diff --git a/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx b/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx index 84a01d508f..d322121cb2 100644 --- a/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx +++ b/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx @@ -5,6 +5,7 @@ import { HStack, Icon, Text, VStack } from "@chakra-ui/react" import { Link } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" import { + BiCog, BiData, BiFile, BiFolder, @@ -43,6 +44,8 @@ export const TitleCell = ({ return BiFolder case ResourceType.Collection: return BiData + case ResourceType.CollectionMeta: + return BiCog case ResourceType.CollectionPage: return BiFile case ResourceType.CollectionLink: diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 5608cdd433..ee8cb961e7 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -153,7 +153,8 @@ export const pageRouter = router({ type !== ResourceType.CollectionPage && type !== ResourceType.RootPage && type !== ResourceType.IndexPage && - type !== ResourceType.FolderMeta + type !== ResourceType.FolderMeta && + type !== ResourceType.CollectionMeta ) { throw new TRPCError({ code: "NOT_FOUND", diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index 1a82609a67..78c3bf43e9 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -179,6 +179,7 @@ export const resourceRouter = router({ .select(["title", "permalink", "type", "id"]) .where("Resource.type", "!=", ResourceType.RootPage) .where("Resource.type", "!=", ResourceType.FolderMeta) + .where("Resource.type", "!=", ResourceType.CollectionMeta) .where("Resource.siteId", "=", Number(siteId)) .$narrowType<{ type: Extract< @@ -336,6 +337,7 @@ export const resourceRouter = router({ .where("Resource.siteId", "=", siteId) .where("Resource.type", "!=", ResourceType.RootPage) .where("Resource.type", "!=", ResourceType.FolderMeta) + .where("Resource.type", "!=", ResourceType.CollectionMeta) .select((eb) => [eb.fn.countAll().as("totalCount")]) if (resourceId) { @@ -356,6 +358,7 @@ export const resourceRouter = router({ .where("Resource.siteId", "=", siteId) .where("Resource.type", "!=", ResourceType.RootPage) .where("Resource.type", "!=", ResourceType.FolderMeta) + .where("Resource.type", "!=", ResourceType.CollectionMeta) .orderBy("Resource.updatedAt", "desc") .orderBy("Resource.title", "asc") .offset(offset) diff --git a/apps/studio/src/server/modules/resource/resource.service.ts b/apps/studio/src/server/modules/resource/resource.service.ts index 1a865ba05d..5108ae4cc6 100644 --- a/apps/studio/src/server/modules/resource/resource.service.ts +++ b/apps/studio/src/server/modules/resource/resource.service.ts @@ -130,8 +130,8 @@ export const getFullPageById = async ( return publishedBlob } -// There are 6 types of pages this get query supports: -// Page, CollectionPage, RootPage, IndexPage, CollectionLink, FolderMeta +// There are 7 types of pages this get query supports: +// Page, CollectionPage, RootPage, IndexPage, CollectionLink, FolderMeta, CollectionMeta export const getPageById = ( db: SafeKysely, args: { resourceId: number; siteId: number }, @@ -145,6 +145,7 @@ export const getPageById = ( eb("type", "=", ResourceType.IndexPage), eb("type", "=", ResourceType.CollectionLink), eb("type", "=", ResourceType.FolderMeta), + eb("type", "=", ResourceType.CollectionMeta), ]), ) .select(defaultResourceSelect) @@ -309,6 +310,7 @@ export const getLocalisedSitemap = async ( return fb("Resource.parentId", "=", String(resource.parentId)) }) .where("Resource.type", "!=", ResourceType.FolderMeta) + .where("Resource.type", "!=", ResourceType.CollectionMeta) .select(defaultResourceSelect), ) // Step 3: Combine all the resources in a single array diff --git a/apps/studio/src/utils/resource.ts b/apps/studio/src/utils/resource.ts index 46c9c89a0a..740d73647d 100644 --- a/apps/studio/src/utils/resource.ts +++ b/apps/studio/src/utils/resource.ts @@ -15,6 +15,7 @@ export const getResourceSubpath = (resourceType: ResourceType) => { case ResourceType.Collection: return "collections" case ResourceType.FolderMeta: + case ResourceType.CollectionMeta: // TODO: Not implemented yet return "" default: diff --git a/apps/studio/src/utils/sitemap.ts b/apps/studio/src/utils/sitemap.ts index 43acb1ca27..f20010bf21 100644 --- a/apps/studio/src/utils/sitemap.ts +++ b/apps/studio/src/utils/sitemap.ts @@ -1,7 +1,7 @@ import type { IsomerSitemap } from "@opengovsg/isomer-components" -import type { Resource } from "@prisma/client" import { ResourceType } from "~prisma/generated/generatedEnums" +import type { Resource } from "~prisma/generated/selectableTypes" import { INDEX_PAGE_PERMALINK } from "~/constants/sitemap" type ResourceDto = Omit< @@ -27,7 +27,8 @@ const getSitemapTreeFromArray = ( return ( resource.parentId === null && resource.type !== ResourceType.RootPage && - resource.type !== ResourceType.FolderMeta + resource.type !== ResourceType.FolderMeta && + resource.type !== ResourceType.CollectionMeta ) } return ( diff --git a/package-lock.json b/package-lock.json index 6dbee76f6e..5b202652c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34439,6 +34439,21 @@ "tooling/typescript": { "name": "@isomer/tsconfig", "version": "0.0.0" + }, + "tooling/template/node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.13.tgz", + "integrity": "sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/components/src/templates/next/components/internal/BlogCard/BlogCard.stories.tsx b/packages/components/src/templates/next/components/internal/BlogCard/BlogCard.stories.tsx new file mode 100644 index 0000000000..81c901004e --- /dev/null +++ b/packages/components/src/templates/next/components/internal/BlogCard/BlogCard.stories.tsx @@ -0,0 +1,113 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { withChromaticModes } from "@isomer/storybook-config" + +import type { CollectionCardProps } from "~/interfaces" +import { Tag } from "~/interfaces/internal/CollectionCard" +import { BlogCard } from "./BlogCard" + +const meta: Meta = { + title: "Next/Internal Components/Blog Card", + component: BlogCard, + argTypes: {}, + parameters: { + layout: "fullscreen", + themes: { + themeOverride: "Isomer Next", + }, + chromatic: withChromaticModes(["desktop", "mobile"]), + }, +} +export default meta +type Story = StoryObj + +const generateArgs = ({ + shouldShowDate = true, + isLastUpdatedUndefined = false, + withoutImage = false, + title = "A journal on microscopic plastic and their correlation to the number of staycations enjoyed per millennials between the ages of 30-42, substantiated by research from IDK university", + description = "We've looked at how people's spending correlates with how much microscopic plastic they consumed over the year. We've looked at how people's spending correlates with how much microscopic plastic they consumed over the year.", + tags = [], +}: { + shouldShowDate?: boolean + isLastUpdatedUndefined?: boolean + withoutImage?: boolean + title?: string + description?: string + tags?: Tag[] +}): Partial & { shouldShowDate?: boolean } => { + return { + lastUpdated: isLastUpdatedUndefined ? undefined : "December 2, 2023", + category: "Research", + title, + description, + image: withoutImage + ? undefined + : { + src: "https://placehold.co/500x500", + alt: "placeholder", + }, + referenceLinkHref: "/", + imageSrc: "https://placehold.co/500x500", + itemTitle: title, + shouldShowDate, + tags, + } +} + +export const Default: Story = { + args: generateArgs({}), +} + +export const UndefinedDate: Story = { + args: generateArgs({ isLastUpdatedUndefined: true }), +} + +export const HideDate: Story = { + args: generateArgs({ + shouldShowDate: false, + isLastUpdatedUndefined: true, + }), +} + +export const CardWithoutImage: Story = { + args: generateArgs({ withoutImage: true }), +} + +export const ShortDescription: Story = { + args: generateArgs({ + title: "Short title", + description: "Short description", + }), +} + +export const TagsWithImage: Story = { + args: generateArgs({ + title: "Collection card with tags", + description: "This is a random description that will be on the card", + tags: [ + { + category: "long", + selected: [ + "This is a very long tag that shuold be reflowed on smaller screens maybe", + ], + }, + ], + }), +} + +export const TagsWithoutImage: Story = { + args: generateArgs({ + title: "Collection card without tags", + withoutImage: true, + description: "This is a random description that will be on the card", + tags: [ + { + category: "very long", + selected: [ + "This is a second long link that should eat into the image area so that we can see how it looks", + ], + }, + ], + }), +} diff --git a/packages/components/src/templates/next/components/internal/BlogCard/BlogCard.tsx b/packages/components/src/templates/next/components/internal/BlogCard/BlogCard.tsx new file mode 100644 index 0000000000..c4d192af6c --- /dev/null +++ b/packages/components/src/templates/next/components/internal/BlogCard/BlogCard.tsx @@ -0,0 +1,93 @@ +"use client" + +import { Text } from "react-aria-components" + +import type { CollectionCardProps } from "~/interfaces" +import type { CollectionPageSchemaType } from "~/types" +import { tv } from "~/lib/tv" +import { focusVisibleHighlight, getFormattedDate } from "~/utils" +import { ImageClient } from "../../complex/Image" +import { Link } from "../Link" +import { Tag } from "../Tag" + +const collectionCardLinkStyle = tv({ + extend: focusVisibleHighlight, + base: "prose-title-md-semibold line-clamp-3 w-fit underline-offset-4 hover:underline", +}) + +export const BlogCard = ({ + LinkComponent, + lastUpdated, + description, + category, + image, + referenceLinkHref, + imageSrc, + itemTitle, + siteAssetsBaseUrl, + shouldShowDate = true, + tags = [], +}: CollectionCardProps & { + shouldShowDate?: boolean + siteAssetsBaseUrl: string | undefined + LinkComponent: CollectionPageSchemaType["LinkComponent"] +}): JSX.Element => { + return ( + // NOTE: In smaller viewports, we render a border between items for easy distinguishing + // and to do that, we add a padding on smaller viewports +
+ {image && ( +
+ { + + } +
+ )} + {shouldShowDate && ( + + {lastUpdated ? getFormattedDate(lastUpdated) : "-"} + + )} +
+

+ + {itemTitle} + +

+ {tags && tags.length > 0 && ( +
+ {tags.flatMap(({ category, selected: labels }) => { + return ( +
+

{category}

+ {labels.map((label) => { + return {label} + })} +
+ ) + })} +
+ )} + {description && ( + + {description} + + )} + {/* TODO: Feature enhancement? Filter by category when clicked */} + + {category} + +
+
+ ) +} diff --git a/packages/components/src/templates/next/components/internal/BlogCard/index.ts b/packages/components/src/templates/next/components/internal/BlogCard/index.ts new file mode 100644 index 0000000000..4d1a958c92 --- /dev/null +++ b/packages/components/src/templates/next/components/internal/BlogCard/index.ts @@ -0,0 +1 @@ +export * from "./BlogCard" diff --git a/packages/components/src/templates/next/components/internal/index.ts b/packages/components/src/templates/next/components/internal/index.ts index 64d041c0c4..02d7f29924 100644 --- a/packages/components/src/templates/next/components/internal/index.ts +++ b/packages/components/src/templates/next/components/internal/index.ts @@ -27,3 +27,4 @@ export { GoogleTagManagerHeader, GoogleTagManagerBody, } from "./GoogleTagManager" +export { BlogCard } from "./BlogCard" diff --git a/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx b/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx index 7e0fb65016..00e14ac11d 100644 --- a/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx +++ b/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx @@ -74,8 +74,10 @@ const COLLECTION_ITEMS: IsomerSitemap[] = flatten( const generateArgs = ({ collectionItems = COLLECTION_ITEMS, + variant = "collection", }: { collectionItems?: IsomerSitemap[] + variant?: CollectionPageSchemaType["page"]["variant"] } = {}): CollectionPageSchemaType => { return { layout: "collection", @@ -263,6 +265,7 @@ const generateArgs = ({ lastModified: "2024-05-02T14:12:57.160Z", subtitle: "Since this page type supports text-heavy articles that are primarily for reading and absorbing information, the max content width on desktop is kept even smaller than its General Content Page counterpart.", + variant, }, content: [], } @@ -416,3 +419,10 @@ export const FileCardNoImage: Story = { ], }), } + +export const Blog: Story = { + args: generateArgs({ + collectionItems: COLLECTION_ITEMS, + variant: "blog", + }), +} diff --git a/packages/components/src/templates/next/layouts/Collection/CollectionClient.tsx b/packages/components/src/templates/next/layouts/Collection/CollectionClient.tsx index ce30911928..0cea68e86d 100644 --- a/packages/components/src/templates/next/layouts/Collection/CollectionClient.tsx +++ b/packages/components/src/templates/next/layouts/Collection/CollectionClient.tsx @@ -3,7 +3,10 @@ import { useRef } from "react" import type { Filter as FilterType } from "../../types/Filter" -import type { CollectionPageSchemaType } from "~/engine" +import type { + CollectionPagePageProps, + CollectionPageSchemaType, +} from "~/engine" import type { BreadcrumbProps, ProcessedCollectionCardProps, @@ -20,7 +23,7 @@ import { CollectionResults } from "./CollectionResults" import { ITEMS_PER_PAGE, useCollection } from "./useCollection" interface CollectionClientProps { - page: CollectionPageSchemaType["page"] + page: CollectionPagePageProps items: ProcessedCollectionCardProps[] filters: FilterType[] shouldShowDate: boolean @@ -120,6 +123,7 @@ const CollectionClient = ({ >
{ shouldShowDate?: boolean + variant?: CollectionVariant siteAssetsBaseUrl: string | undefined LinkComponent: CollectionPageSchemaType["LinkComponent"] } +const collection = tv( + { + slots: { + collectionResults: "flex w-full flex-col gap-0", + }, + variants: { + variant: { + collection: { + collectionResults: "flex w-full flex-col gap-0", + }, + blog: { + collectionResults: + // NOTE: we remove the gap so that the blog cards can + // render their own border between each item + "grid grid-cols-1 sm:gap-0 md:grid-cols-2 md:gap-5", + }, + }, + }, + }, + { responsiveVariants: ["md", "sm", "lg"] }, +) + export const CollectionResults = ({ paginatedItems, searchValue, @@ -27,7 +51,10 @@ export const CollectionResults = ({ shouldShowDate = true, siteAssetsBaseUrl, LinkComponent, + variant = "collection", }: CollectionResultProps) => { + const { collectionResults } = collection({ variant }) + if (totalCount === 0) { return (

@@ -54,17 +81,27 @@ export const CollectionResults = ({

{/* NOTE: DO NOT add h-full to this div as it will break old browsers */} -
+
{paginatedItems.length > 0 && - paginatedItems.map((item) => ( - - ))} + paginatedItems.map((item) => + variant === "collection" ? ( + + ) : ( + + ), + )} {paginatedItems.length === 0 && (

diff --git a/packages/components/src/types/index.ts b/packages/components/src/types/index.ts index d0b445af4f..f737614f80 100644 --- a/packages/components/src/types/index.ts +++ b/packages/components/src/types/index.ts @@ -5,3 +5,4 @@ export * from "./schema" export type * from "./site" export type * from "./utils" export type { IsomerSitemap } from "./sitemap" +export * from "./variants" diff --git a/packages/components/src/types/page.ts b/packages/components/src/types/page.ts index 6dae47a5c0..04569d07ab 100644 --- a/packages/components/src/types/page.ts +++ b/packages/components/src/types/page.ts @@ -1,5 +1,6 @@ import type { Static } from "@sinclair/typebox" import { Type } from "@sinclair/typebox" +import { Collection } from "react-aria-components" import { ArticlePageHeaderSchema, @@ -8,6 +9,7 @@ import { } from "~/interfaces" import { AltTextSchema, generateImageSrcSchema } from "~/interfaces/complex" import { REF_HREF_PATTERN } from "~/utils/validation" +import { CollectionVariant, CollectionVariantSchema } from "./variants" const categorySchemaObject = Type.Object({ category: Type.String({ @@ -117,11 +119,16 @@ interface ArticlePageAdditionalProps { tags?: CollectionPagePageProps["tags"] } +interface CollectionVariantProps { + variant?: CollectionVariant +} + export type ArticlePagePageProps = Static & BasePageAdditionalProps & ArticlePageAdditionalProps export type CollectionPagePageProps = Static & - BasePageAdditionalProps + BasePageAdditionalProps & + CollectionVariantProps export type ContentPagePageProps = Static & BasePageAdditionalProps export type DatabasePagePageProps = Static & diff --git a/packages/components/src/types/variants.ts b/packages/components/src/types/variants.ts new file mode 100644 index 0000000000..922cf8c1f3 --- /dev/null +++ b/packages/components/src/types/variants.ts @@ -0,0 +1,14 @@ +import { Static, Type } from "@sinclair/typebox" + +export const CollectionVariantSchema = Type.Union([ + Type.Literal("blog"), + Type.Literal("collection"), +]) + +export type CollectionVariant = Static + +export interface IsomerLayoutVariants { + collection: { + variant: CollectionVariant + } +} diff --git a/tooling/build/scripts/generate-sitemap.js b/tooling/build/scripts/generate-sitemap.js index e0292c5f58..fc2c885b44 100644 --- a/tooling/build/scripts/generate-sitemap.js +++ b/tooling/build/scripts/generate-sitemap.js @@ -71,6 +71,7 @@ const getSiteMapEntry = async (fullPath, relativePath, name) => { category: schemaData.page.category, date: schemaData.page.date, image: schemaData.page.image, + tags: schemaData.page.tags, } if (schemaData.layout === "file") { diff --git a/tooling/build/scripts/publishing/index.ts b/tooling/build/scripts/publishing/index.ts index 4ed3b9803b..afba54ee9b 100644 --- a/tooling/build/scripts/publishing/index.ts +++ b/tooling/build/scripts/publishing/index.ts @@ -31,7 +31,7 @@ const SITE_ID = Number(process.env.SITE_ID) // Guaranteed to not be present in the database because we start from 1 const DANGLING_DIRECTORY_PAGE_ID = "-1" const INDEX_PAGE_PERMALINK = "_index" -const PAGE_ORDER_PERMALINK = "_meta" +const META_PERMALINK = "_meta" const PAGE_RESOURCE_TYPES = [ "Page", "CollectionPage", @@ -50,8 +50,8 @@ const getConvertedPermalink = (fullPermalink: string) => { // we prohibit users from using `_` as a character const fullPermalinkWithoutIndex = fullPermalink.endsWith(INDEX_PAGE_PERMALINK) ? fullPermalink.slice(0, -INDEX_PAGE_PERMALINK.length) - : fullPermalink.endsWith(PAGE_ORDER_PERMALINK) - ? fullPermalink.slice(0, -PAGE_ORDER_PERMALINK.length) + : fullPermalink.endsWith(META_PERMALINK) + ? fullPermalink.slice(0, -META_PERMALINK.length) : fullPermalink if (fullPermalinkWithoutIndex.endsWith("/")) { @@ -270,7 +270,7 @@ function generateSitemapTree( title, permalink: `${pathPrefix.length === 1 ? "" : pathPrefix}/${danglingDirectory}`, lastModified: new Date().toISOString(), - layout: "index", + layout: folder?.type === "Collection" ? "collection" : "index", summary: `Pages in ${title}`, type: folder?.type ?? ResourceType.Folder, } @@ -293,8 +293,8 @@ function generateSitemapTree( resource.type === "FolderMeta" && resource.fullPermalink === (pathPrefixWithoutLeadingSlash.length === 0 - ? PAGE_ORDER_PERMALINK - : `${pathPrefixWithoutLeadingSlash}/${PAGE_ORDER_PERMALINK}`), + ? META_PERMALINK + : `${pathPrefixWithoutLeadingSlash}/${META_PERMALINK}`), )?.content?.order children.sort((a, b) => { @@ -377,8 +377,15 @@ async function processDanglingDirectories( const content = getFolderIndexPageContents(title) return { title, permalink, content } }), - ...collections.map(({ title, permalink }) => { - const content = getCollectionIndexPageContents(title) + ...collections.map(({ id, title, permalink }) => { + const meta = resources.find( + ({ type, parentId }) => + parentId === Number(id) && type === "CollectionMeta", + ) + const content = getCollectionIndexPageContents( + title, + meta?.content.variant, + ) return { title, permalink, content } }), ].map((child) => { diff --git a/tooling/build/scripts/publishing/queries.ts b/tooling/build/scripts/publishing/queries.ts index 6e15adc0b8..d9a1ea42da 100644 --- a/tooling/build/scripts/publishing/queries.ts +++ b/tooling/build/scripts/publishing/queries.ts @@ -8,7 +8,7 @@ WITH RECURSIVE "resourcePath" (id, title, permalink, parentId, type, content, "f r."parentId", r.type, CASE - WHEN r.type IN ('Page', 'CollectionPage', 'CollectionLink', 'IndexPage', 'RootPage', 'FolderMeta') THEN b."content" + WHEN r.type IN ('Page', 'CollectionPage', 'CollectionLink', 'IndexPage', 'RootPage', 'FolderMeta', 'CollectionMeta') THEN b."content" ELSE NULL END AS content, r.permalink AS "fullPermalink", @@ -30,7 +30,7 @@ WITH RECURSIVE "resourcePath" (id, title, permalink, parentId, type, content, "f r."parentId", r.type, CASE - WHEN r.type IN ('Page', 'CollectionPage', 'CollectionLink', 'IndexPage', 'RootPage', 'FolderMeta') THEN b."content" + WHEN r.type IN ('Page', 'CollectionPage', 'CollectionLink', 'IndexPage', 'RootPage', 'FolderMeta', 'CollectionMeta') THEN b."content" ELSE NULL END AS content, CONCAT(path."fullPermalink", '/', r.permalink) AS "fullPermalink", diff --git a/tooling/build/scripts/publishing/tsconfig.json b/tooling/build/scripts/publishing/tsconfig.json index d350ce5de1..cbf02b0cdf 100644 --- a/tooling/build/scripts/publishing/tsconfig.json +++ b/tooling/build/scripts/publishing/tsconfig.json @@ -15,7 +15,10 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */, "paths": { - "~generated/*": ["../../../../apps/studio/prisma/generated/*"] + "~generated/*": ["../../../../apps/studio/prisma/generated/*"], + // NOTE: this works because we clone our repo as a whole. + // Respect the package's default export and use only index + "~schema": ["../../../../packages/components/src/index.ts"] } } } diff --git a/tooling/build/scripts/publishing/utils/getIndexPageContent.ts b/tooling/build/scripts/publishing/utils/getIndexPageContent.ts index cb611aa694..c256459b70 100644 --- a/tooling/build/scripts/publishing/utils/getIndexPageContent.ts +++ b/tooling/build/scripts/publishing/utils/getIndexPageContent.ts @@ -1,3 +1,5 @@ +import { CollectionPagePageProps } from "~schema" + const ISOMER_SCHEMA_VERSION = "0.1.0" // Generate the index page content for a given folder @@ -13,7 +15,10 @@ export const getFolderIndexPageContents = (title: string) => ({ content: [], }) -export const getCollectionIndexPageContents = (title: string) => ({ +export const getCollectionIndexPageContents = ( + title: string, + variant: CollectionPagePageProps["variant"] = "collection", +) => ({ version: ISOMER_SCHEMA_VERSION, layout: "collection", page: { @@ -21,6 +26,7 @@ export const getCollectionIndexPageContents = (title: string) => ({ contentPageHeader: { summary: `Pages in ${title}`, }, + variant, }, content: [], })