diff --git a/models/FeaturedPostsSection.yaml b/models/FeaturedPostsSection.yaml
index b085f142..ac7a03d2 100644
--- a/models/FeaturedPostsSection.yaml
+++ b/models/FeaturedPostsSection.yaml
@@ -4,52 +4,16 @@ label: Featured posts
labelField: title
thumbnail: https://assets.stackbit.com/components/models/thumbnails/default.png
extends:
- - Section
+ - PostFeedSection
groups:
- sectionComponent
-fieldGroups:
- - name: styles
- label: Styles
- - name: settings
- label: Settings
fields:
- - type: enum
- name: variant
- group: styles
- label: Arrangement
- options:
- - label: Three columns grid
- value: variant-a
- - label: Two columns grid
- value: variant-b
- - label: Mixed grid
- value: variant-c
- - label: List
- value: variant-d
- default: variant-a
- - name: colors
- default: colors-h
- - type: string
- name: title
- label: Title
+ - name: title
default: Featured
- - type: string
- name: subtitle
- label: Subtitle
+ - name: subtitle
default: Featured blog posts section example
- - type: list
- name: actions
- label: Actions
- items:
- type: model
- models:
- - Button
- - Link
- default:
- - type: Button
- label: View all
- url: '/'
- style: primary
+ - name: colors
+ default: colors-h
- type: list
name: posts
label: Posts
@@ -61,69 +25,3 @@ fields:
- content/pages/blog/post-three.md
- content/pages/blog/post-two.md
- content/pages/blog/post-one.md
- - type: boolean
- name: showDate
- label: Show post date
- default: false
- - type: boolean
- name: showAuthor
- label: Show post author
- description: Show the author of the post
- default: false
- - type: style
- name: styles
- styles:
- self:
- height: ['auto', 'screen']
- width: ['narrow', 'wide', 'full']
- margin: ['tw0:36']
- padding: ['tw4:36']
- justifyContent: ['flex-start', 'flex-end', 'center']
- borderRadius: '*'
- borderWidth: ['0:8']
- borderStyle: '*'
- borderColor:
- - value: 'border-primary'
- label: 'Primary color'
- color: '$primary'
- - value: 'border-secondary'
- label: 'Secondary color'
- color: '$secondary'
- - value: 'border-dark'
- label: 'Dark color'
- color: '$dark'
- - value: 'border-complementary'
- label: 'Complementary color'
- color: '$complementary'
- - value: 'border-complementary-alt'
- label: 'Complementary alt color'
- color: '$complementaryAlt'
- title:
- fontWeight: ['400', '700']
- fontStyle: ['normal', 'italic']
- textAlign: ['left', 'center', 'right']
- subtitle:
- fontWeight: ['400', '700']
- fontStyle: ['normal', 'italic']
- textAlign: ['left', 'center', 'right']
- actions:
- justifyContent: ['flex-start', 'flex-end', 'center']
- default:
- self:
- height: auto
- width: wide
- margin: ['mt-0', 'mb-0', 'ml-0', 'mr-0']
- padding: ['pt-12', 'pb-12', 'pl-4', 'pr-4']
- justifyContent: center
- borderRadius: none
- borderWidth: 0
- borderStyle: none
- borderColor: border-dark
- title:
- textAlign: center
- subtitle:
- fontWeight: 400
- fontStyle: normal
- textAlign: center
- actions:
- justifyContent: center
diff --git a/models/PagedPostsSection.yaml b/models/PagedPostsSection.yaml
new file mode 100644
index 00000000..3ebdf1f8
--- /dev/null
+++ b/models/PagedPostsSection.yaml
@@ -0,0 +1,29 @@
+type: object
+name: PagedPostsSection
+label: Post feed
+labelField: title
+extends:
+ - PostFeedSection
+fields:
+ - name: title
+ hidden: true
+ default: null
+ - name: subtitle
+ hidden: true
+ default: null
+ - name: showDate
+ default: true
+ - name: showAuthor
+ default: true
+ - name: variant
+ default: variant-d
+ options:
+ - label: Three columns grid
+ value: variant-a
+ - label: List
+ value: variant-d
+ - name: actions
+ hidden: true
+ default: []
+ - name: colors
+ default: colors-a
diff --git a/models/Person.yaml b/models/Person.yaml
index e5a2e757..03bd5c5e 100644
--- a/models/Person.yaml
+++ b/models/Person.yaml
@@ -26,8 +26,7 @@ fields:
default:
url: https://assets.stackbit.com/components/images/default/default-person.png
altText: Person photo
- - type: string
- name: id
- label: ID
- default: name-surname
- required: true
+ - type: slug
+ name: slug
+ label: Slug
+ description: "Slug used to render posts written by this person. For example, if the slug is 'john-doe', a page will be created under /blog/author/john-doe"
diff --git a/models/PostFeedCategoryLayout.yaml b/models/PostFeedCategoryLayout.yaml
new file mode 100644
index 00000000..67c872dd
--- /dev/null
+++ b/models/PostFeedCategoryLayout.yaml
@@ -0,0 +1,6 @@
+name: PostFeedCategoryLayout
+label: Blog Category
+layout: PostFeedCategoryLayout
+hideContent: true
+extends:
+ - PostFeedLayout
diff --git a/models/PostFeedLayout.yaml b/models/PostFeedLayout.yaml
new file mode 100644
index 00000000..31467325
--- /dev/null
+++ b/models/PostFeedLayout.yaml
@@ -0,0 +1,42 @@
+name: PostFeedLayout
+label: Blog
+labelField: title
+layout: PostFeedLayout
+hideContent: true
+fields:
+ - type: string
+ name: title
+ label: Title
+ default: This is a page title
+ - type: number
+ name: numOfPostsPerPage
+ label: Number of Posts per page
+ description: set to 0 to show all posts on a single page
+ default: 10
+ - type: model
+ name: postFeed
+ readOnly: true
+ label: Post Feed
+ models: [PagedPostsSection]
+ default:
+ title: null
+ subtitle: null
+ showDate: true
+ showAuthor: true
+ variant: variant-d
+ colors: colors-a
+ actions: []
+ - type: list
+ name: topSections
+ label: Top Sections
+ items:
+ type: model
+ groups:
+ - sectionComponent
+ - type: list
+ name: bottomSections
+ label: Bottom Sections
+ items:
+ type: model
+ groups:
+ - sectionComponent
diff --git a/models/PostFeedSection.yaml b/models/PostFeedSection.yaml
index d87ac693..bbb55c52 100644
--- a/models/PostFeedSection.yaml
+++ b/models/PostFeedSection.yaml
@@ -4,34 +4,43 @@ label: Post feed
labelField: title
extends:
- Section
-groups:
- - sectionComponent
fieldGroups:
- name: styles
label: Styles
- name: settings
label: Settings
fields:
+ - type: string
+ name: title
+ label: Title
+ default: Posts
+ - type: string
+ name: subtitle
+ label: Subtitle
+ default: Blog posts
+ - type: boolean
+ name: showDate
+ label: Show post date
+ default: false
+ - type: boolean
+ name: showAuthor
+ label: Show post author
+ description: Show the author of the post
+ default: false
- type: enum
name: variant
group: styles
label: Arrangement
+ default: variant-a
options:
- label: Three columns grid
value: variant-a
- - label: List
+ - label: Two columns grid
value: variant-b
- default: variant-a
- - name: colors
- default: colors-h
- - type: string
- name: title
- label: Title
- default: Latest news
- - type: string
- name: subtitle
- label: Subtitle
- default: Latest blog posts section example
+ - label: Mixed grid
+ value: variant-c
+ - label: List
+ value: variant-d
- type: list
name: actions
label: Actions
@@ -45,15 +54,6 @@ fields:
label: View all
url: '/'
style: primary
- - type: boolean
- name: showRecent
- label: Show recent posts only
- description: Show the specified number of recent posts
- default: false
- - type: number
- name: recentCount
- label: Number of recent posts to show
- default: 6
- type: style
name: styles
styles:
diff --git a/models/PostLayout.yaml b/models/PostLayout.yaml
index 63012c51..d0d9357e 100644
--- a/models/PostLayout.yaml
+++ b/models/PostLayout.yaml
@@ -12,6 +12,11 @@ fields:
name: date
label: Date
required: true
+ - type: reference
+ name: category
+ label: Category
+ models:
+ - PostFeedCategoryLayout
- type: reference
name: author
label: Author
diff --git a/models/RecentPostsSection.yaml b/models/RecentPostsSection.yaml
new file mode 100644
index 00000000..bfef4272
--- /dev/null
+++ b/models/RecentPostsSection.yaml
@@ -0,0 +1,26 @@
+type: object
+name: RecentPostsSection
+label: Recent posts
+labelField: title
+extends:
+ - PostFeedSection
+groups:
+ - sectionComponent
+fields:
+ - name: title
+ default: Recent Posts
+ - name: subtitle
+ default: Latest blog posts section example
+ - name: variant
+ default: variant-a
+ options:
+ - label: Three columns grid
+ value: variant-a
+ - label: List
+ value: variant-d
+ - name: colors
+ default: colors-h
+ - type: number
+ name: recentCount
+ label: Number of recent posts to show
+ default: 6
diff --git a/src/components-manifest.json b/src/components-manifest.json
index 60493634..32c2f41f 100644
--- a/src/components-manifest.json
+++ b/src/components-manifest.json
@@ -44,11 +44,6 @@
"modelName": "FeaturedPeopleSection",
"isDynamic": true
},
- "FeaturedPostsSection": {
- "path": "components/FeaturedPostsSection",
- "modelName": "FeaturedPostsSection",
- "isDynamic": true
- },
"Footer": {
"path": "components/Footer",
"modelName": "Footer",
@@ -84,6 +79,16 @@
"modelName": "PostFeedSection",
"isDynamic": true
},
+ "FeaturedPostsSection": {
+ "path": "components/FeaturedPostsSection",
+ "modelName": "FeaturedPostsSection",
+ "isDynamic": true
+ },
+ "RecentPostsSection": {
+ "path": "components/RecentPostsSection",
+ "modelName": "RecentPostsSection",
+ "isDynamic": true
+ },
"QuoteSection": {
"path": "components/QuoteSection",
"modelName": "QuoteSection",
@@ -133,5 +138,15 @@
"path": "layouts/PostLayout",
"modelName": "PostLayout",
"isDynamic": true
+ },
+ "PostFeedLayout": {
+ "path": "layouts/PostFeedLayout",
+ "modelName": "PostFeedLayout",
+ "isDynamic": true
+ },
+ "PostFeedCategoryLayout": {
+ "path": "layouts/PostFeedCategoryLayout",
+ "modelName": "PostFeedCategoryLayout",
+ "isDynamic": true
}
}
diff --git a/src/components/ContactSection/index.tsx b/src/components/ContactSection/index.tsx
index c085d3a1..56ca7719 100644
--- a/src/components/ContactSection/index.tsx
+++ b/src/components/ContactSection/index.tsx
@@ -3,6 +3,7 @@ import Markdown from 'markdown-to-jsx';
import classNames from 'classnames';
import { getComponent } from '../../components-registry';
import { mapStylesToClassNames as mapStyles } from '../../utils/map-styles-to-class-names';
+import { getDataAttrs } from '../../utils/get-data-attrs';
import FormBlock from '../FormBlock';
export default function ContactSection(props) {
@@ -13,6 +14,7 @@ export default function ContactSection(props) {
return (
-
-
- {featuredPostsHeader(props)}
- {featuredPostsVariants(props)}
- {featuredPostsActions(props)}
-
-
-
- );
-}
-
-function featuredPostsHeader(props) {
- if (!props.title && !props.subtitle) {
- return null;
- }
- const styles = props.styles || {};
- return (
-
- {props.title && (
-
- {props.title}
-
- )}
- {props.subtitle && (
-
- {props.subtitle}
-
- )}
-
- );
-}
-
-function featuredPostsActions(props) {
- const actions = props.actions || [];
- if (actions.length === 0) {
- return null;
- }
- const styles = props.styles || {};
- const Action = getComponent('Action');
- return (
-
- {props.actions.map((action, index) => (
-
- ))}
-
- );
-}
-
-function featuredPostsVariants(props) {
- const variant = props.variant || 'variant-a';
- switch (variant) {
- case 'variant-a':
- return postsVariantA(props);
- case 'variant-b':
- return postsVariantB(props);
- case 'variant-c':
- return postsVariantC(props);
- case 'variant-d':
- return postsVariantD(props);
- }
- return null;
-}
-
-function postsVariantA(props) {
- const posts = props.posts || [];
- if (posts.length === 0) {
- return null;
- }
- const ImageBlock = getComponent('ImageBlock');
- return (
-
- {posts.map((post, index) => (
-
- {post.featuredImage && (
-
-
-
- )}
-
- {props.showDate && postDate(post.date)}
-
-
- {post.title}
-
-
- {props.showAuthor && post.author && postAuthor(post.author)}
- {post.excerpt && (
-
- {post.excerpt}
-
- )}
-
-
- ))}
-
- );
-}
-
-function postsVariantB(props) {
- const posts = props.posts || [];
- if (posts.length === 0) {
- return null;
- }
- const ImageBlock = getComponent('ImageBlock');
- return (
-
- {posts.map((post, index) => (
-
- {post.featuredImage && (
-
-
-
- )}
-
- {props.showDate && postDate(post.date)}
-
-
- {post.title}
-
-
- {props.showAuthor && post.author && postAuthor(post.author)}
- {post.excerpt && (
-
- {post.excerpt}
-
- )}
-
-
- ))}
-
- );
-}
-
-function postsVariantC(props) {
- const posts = props.posts || [];
- if (posts.length === 0) {
- return null;
- }
- const ImageBlock = getComponent('ImageBlock');
- return (
-
- {posts.map((post, index) => {
- const isFullWidth = index % 4 === 0;
- return (
-
- {post.featuredImage && (
-
-
-
-
-
- )}
-
- {props.showDate && postDate(post.date)}
-
-
- {post.title}
-
-
- {props.showAuthor && post.author && postAuthor(post.author)}
- {post.excerpt && (
-
- {post.excerpt}
-
- )}
-
-
- );
- })}
-
- );
-}
-
-function postsVariantD(props) {
- const posts = props.posts || [];
- if (posts.length === 0) {
- return null;
- }
- const ImageBlock = getComponent('ImageBlock');
- return (
-
- {posts.map((post, index) => (
-
- {post.featuredImage && (
-
-
-
-
-
- )}
-
- {props.showDate && postDate(post.date)}
-
-
- {post.title}
-
-
- {props.showAuthor && post.author && postAuthor(post.author)}
- {post.excerpt && (
-
- {post.excerpt}
-
- )}
-
-
- ))}
-
- );
-}
-
-function postDate(date) {
- const dateTimeAttr = dayjs(date).format('YYYY-MM-DD HH:mm:ss');
- const formattedDate = dayjs(date).format('MMMM D, YYYY');
- return (
-
-
- {formattedDate}
-
-
- );
-}
-
-function postAuthor(author) {
- return (
-
- By{' '}
-
- {author.firstName && {author.firstName} }{' '}
- {author.lastName && {author.lastName} }
-
-
- );
-}
-
-function mapMinHeightStyles(height) {
- switch (height) {
- case 'auto':
- return 'min-h-0';
- case 'screen':
- return 'min-h-screen';
- }
- return null;
-}
-
-function mapMaxWidthStyles(width) {
- switch (width) {
- case 'narrow':
- return 'max-w-screen-md';
- case 'wide':
- return 'max-w-screen-xl';
- case 'full':
- return 'max-w-full';
- }
- return null;
-}
+import PostFeedSection from '../PostFeedSection';
+export default PostFeedSection;
diff --git a/src/components/HeroSection/index.tsx b/src/components/HeroSection/index.tsx
index b88e45cd..cc545ee6 100644
--- a/src/components/HeroSection/index.tsx
+++ b/src/components/HeroSection/index.tsx
@@ -3,6 +3,7 @@ import Markdown from 'markdown-to-jsx';
import classNames from 'classnames';
import { getComponent } from '../../components-registry';
import { mapStylesToClassNames as mapStyles } from '../../utils/map-styles-to-class-names';
+import { getDataAttrs } from '../../utils/get-data-attrs';
export default function HeroSection(props) {
const cssId = props.elementId || null;
@@ -12,6 +13,7 @@ export default function HeroSection(props) {
return (
-
-
+
+
{postFeedHeader(props)}
{postFeedVariants(props)}
{postFeedActions(props)}
+ {props.pageLinks}
@@ -95,6 +101,10 @@ function postFeedVariants(props) {
return postsVariantA(props);
case 'variant-b':
return postsVariantB(props);
+ case 'variant-c':
+ return postsVariantC(props);
+ case 'variant-d':
+ return postsVariantD(props);
}
return null;
}
@@ -106,33 +116,30 @@ function postsVariantA(props) {
}
const ImageBlock = getComponent('ImageBlock');
return (
-
- {posts.map((post, index) => {
- const dateTimeAttr = dayjs(post.date).format('YYYY-MM-DD HH:mm:ss');
- const formattedDate = dayjs(post.date).format('MMMM D, YYYY');
- return (
-
- {post.featuredImage && (
-
-
+
+ {posts.map((post, index) => (
+
+ {post.featuredImage && (
+
+
+
+ )}
+
+ {props.showDate &&
}
+
+
+ {post.title}
+
+
+ {post.excerpt && (
+
+ {post.excerpt}
+
)}
-
-
-
- {post.title}
-
-
-
-
- {formattedDate}
-
-
- {post.excerpt &&
{post.excerpt}
}
-
-
- );
- })}
+
+
+ ))}
);
}
@@ -144,35 +151,96 @@ function postsVariantB(props) {
}
const ImageBlock = getComponent('ImageBlock');
return (
-
+
+ {posts.map((post, index) => (
+
+ {post.featuredImage && (
+
+
+
+ )}
+
+ {props.showDate &&
}
+
+
+ {post.title}
+
+
+
+ {post.excerpt && (
+
+ {post.excerpt}
+
+ )}
+
+
+ ))}
+
+ );
+}
+
+function postsVariantC(props) {
+ const posts = props.posts || [];
+ if (posts.length === 0) {
+ return null;
+ }
+ const ImageBlock = getComponent('ImageBlock');
+ return (
+
{posts.map((post, index) => {
- const dateTimeAttr = dayjs(post.date).format('YYYY-MM-DD HH:mm:ss');
- const formattedDate = dayjs(post.date).format('MMMM D, YYYY');
+ const isFullWidth = index % 4 === 0;
return (
-
+
{post.featuredImage && (
-
+
)}
-
-
+
+ {props.showDate &&
}
+
{post.title}
-
-
- {formattedDate}
-
-
- {post.excerpt &&
{post.excerpt}
}
+
+ {post.excerpt && (
+
+ {post.excerpt}
+
+ )}
);
@@ -181,6 +249,121 @@ function postsVariantB(props) {
);
}
+function postsVariantD(props) {
+ const posts = props.posts || [];
+ if (posts.length === 0) {
+ return null;
+ }
+ const ImageBlock = getComponent('ImageBlock');
+ return (
+
+ {posts.map((post, index) => (
+
+ {post.featuredImage && (
+
+
+
+
+
+ )}
+
+ {props.showDate &&
}
+
+
+ {post.title}
+
+
+
+ {post.excerpt && (
+
+ {post.excerpt}
+
+ )}
+
+
+ ))}
+
+ );
+}
+
+function PostDate({ post }) {
+ if (!post.date) {
+ return null;
+ }
+ const date = post.date;
+ const dateTimeAttr = dayjs(date).format('YYYY-MM-DD HH:mm:ss');
+ const formattedDate = dayjs(date).format('MMMM D, YYYY');
+ return (
+
+
+ {formattedDate}
+
+
+ );
+}
+
+function PostAttribution({ showAuthor, post }) {
+ const author = showAuthor ? postAuthor(post) : null;
+ const category = postCategory(post);
+ if (!author && !category) {
+ return null;
+ }
+ return (
+
+ {author && (
+ <>
+ {'By '}
+ {author}
+ >
+ )}
+ {category && (
+ <>
+ {author ? ' in ' : 'In '}
+ {category}
+ >
+ )}
+
+ );
+}
+
+function postAuthor(post) {
+ if (!post.author) {
+ return null;
+ }
+ const author = post.author;
+ const children = (
+ <>
+ {author.firstName && {author.firstName} }{' '}
+ {author.lastName && {author.lastName} }
+ >
+ );
+ if (author.slug) {
+ return (
+
+ {children}
+
+ );
+ } else {
+ return {children} ;
+ }
+}
+
+function postCategory(post) {
+ if (!post.category) {
+ return null;
+ }
+ const category = post.category;
+ return (
+
+ {category.title}
+
+ );
+}
+
function mapMinHeightStyles(height) {
switch (height) {
case 'auto':
diff --git a/src/components/QuoteSection/index.tsx b/src/components/QuoteSection/index.tsx
index a0aa7e17..7f3bf843 100644
--- a/src/components/QuoteSection/index.tsx
+++ b/src/components/QuoteSection/index.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import Markdown from 'markdown-to-jsx';
import classNames from 'classnames';
import { mapStylesToClassNames as mapStyles } from '../../utils/map-styles-to-class-names';
+import { getDataAttrs } from '../../utils/get-data-attrs';
export default function QuoteSection(props) {
const cssId = props.elementId || null;
@@ -11,6 +12,7 @@ export default function QuoteSection(props) {
return (
-
-
- );
+ return ;
})}
)}
diff --git a/src/layouts/PostFeedCategoryLayout/index.tsx b/src/layouts/PostFeedCategoryLayout/index.tsx
new file mode 100644
index 00000000..4e6b6605
--- /dev/null
+++ b/src/layouts/PostFeedCategoryLayout/index.tsx
@@ -0,0 +1,2 @@
+import PostFeedLayout from '../PostFeedLayout';
+export default PostFeedLayout;
diff --git a/src/layouts/PostFeedLayout/index.tsx b/src/layouts/PostFeedLayout/index.tsx
new file mode 100644
index 00000000..99700ee9
--- /dev/null
+++ b/src/layouts/PostFeedLayout/index.tsx
@@ -0,0 +1,127 @@
+import * as React from 'react';
+import classNames from 'classnames';
+import Link from '../../utils/link';
+import { getComponent } from '../../components-registry';
+import { getBaseLayoutComponent } from '../../utils/base-layout';
+
+export default function PostFeedLayout(props) {
+ const { page, site } = props;
+ const BaseLayout = getBaseLayoutComponent(page.baseLayout, site.baseLayout);
+ const { title, topSections = [], bottomSections = [], pageIndex, baseUrlPath, numOfPages, items, postFeed } = page;
+ const PostFeedSection = getComponent('PostFeedSection');
+ const pageLinks = PageLinks({ pageIndex, baseUrlPath, numOfPages });
+
+ return (
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {renderSections(topSections, 'topSections')}
+
+ {renderSections(bottomSections, 'bottomSections')}
+
+
+ );
+}
+
+function renderSections(sections: any[], fieldName: string) {
+ if (sections.length === 0) {
+ return null;
+ }
+ return (
+
+ {sections.map((section, index) => {
+ const Component = getComponent(section.type);
+ if (!Component) {
+ throw new Error(`no component matching the page section's type: ${section.type}`);
+ }
+ return ;
+ })}
+
+ );
+}
+
+function PageLinks({ pageIndex, baseUrlPath, numOfPages }) {
+ if (numOfPages < 2) {
+ return null;
+ }
+ const pageLinks = [];
+ const padRange = 2;
+ const startIndex = pageIndex - padRange > 2 ? pageIndex - padRange : 0;
+ const endIndex = pageIndex + padRange < numOfPages - 3 ? pageIndex + padRange : numOfPages - 1;
+
+ // following logic renders pagination controls:
+ // for example, if the current page is 6 (pageIndex === 5)
+ // ↓
+ // ← 1 ... 4 5 6 7 8 ... 20 →
+ // ↑ ↑
+ // and padRange === 2, then it renders from 4 (6 - 2) to 8 (6 + 2)
+
+ // renders prev "←" button, if the current page is the first page, the button is disabled
+ if (pageIndex > 0) {
+ pageLinks.push(
);
+ } else {
+ pageLinks.push(
);
+ }
+
+ // if startIndex is not 0, then render the first page followed by ellipsis, if needed.
+ if (startIndex > 0) {
+ pageLinks.push(
);
+ if (startIndex > 1) {
+ pageLinks.push(
);
+ }
+ }
+
+ // render all pages between startIndex and endIndex, the current page should be disabled
+ for (let i = startIndex; i <= endIndex; i++) {
+ if (pageIndex === i) {
+ pageLinks.push(
);
+ } else {
+ pageLinks.push(
);
+ }
+ }
+
+ // if endIndex is not the last page, then render the last page preceded by ellipsis, if needed.
+ if (endIndex < numOfPages - 1) {
+ if (endIndex < numOfPages - 2) {
+ pageLinks.push(
);
+ }
+ pageLinks.push(
);
+ }
+
+ // renders next "→" button, if the current page is the last page, the button is disabled
+ if (pageIndex < numOfPages - 1) {
+ pageLinks.push(
);
+ } else {
+ pageLinks.push(
);
+ }
+
+ return
{pageLinks}
;
+}
+
+function PageLink({ pageIndex, buttonLabel, baseUrlPath }) {
+ return (
+
+ {buttonLabel}
+
+ );
+}
+
+function PageLinkDisabled({ buttonLabel }) {
+ return (
+
+ {buttonLabel}
+
+ );
+}
+
+function Ellipsis() {
+ return
… ;
+}
+
+function urlPathForPageAtIndex(pageIndex, baseUrlPath) {
+ return pageIndex === 0 ? baseUrlPath : `${baseUrlPath}/page/${pageIndex + 1}`;
+}
diff --git a/src/layouts/PostLayout/index.tsx b/src/layouts/PostLayout/index.tsx
index 116d1ac3..05d1ff86 100644
--- a/src/layouts/PostLayout/index.tsx
+++ b/src/layouts/PostLayout/index.tsx
@@ -3,6 +3,8 @@ import dayjs from 'dayjs';
import Markdown from 'markdown-to-jsx';
import { getBaseLayoutComponent } from '../../utils/base-layout';
import { getComponent } from '../../components-registry';
+import Link from '../../utils/link';
+import getPageUrlPath from '../../utils/get-page-url-path';
export default function PostLayout(props) {
const { page, site } = props;
@@ -23,7 +25,7 @@ export default function PostLayout(props) {
{page.title && {page.title} }
- {page.author && postAuthor(page.author)}
+
{page.markdown_content && (
@@ -39,11 +41,7 @@ export default function PostLayout(props) {
if (!Component) {
throw new Error(`no component matching the page section's type: ${section.type}`);
}
- return (
-
-
-
- );
+ return ;
})}
)}
@@ -52,14 +50,50 @@ export default function PostLayout(props) {
);
}
-function postAuthor(author) {
+function PostAttribution({ post }) {
+ if (!post.author && !post.category) {
+ return null;
+ }
+ const author = post.author ? postAuthor(post.author) : null;
+ const category = post.category ? postCategory(post.category) : null;
return (
-
- By{' '}
-
- {author.firstName && {author.firstName} }{' '}
- {author.lastName && {author.lastName} }
-
+
+ {author && (
+ <>
+ {'By '}
+ {author}
+ >
+ )}
+ {category && (
+ <>
+ {author ? ' in ' : 'In '}
+ {category}
+ >
+ )}
);
}
+
+function postAuthor(author) {
+ const children = (
+ <>
+ {author.firstName &&
{author.firstName} }{' '}
+ {author.lastName &&
{author.lastName} }
+ >
+ );
+ return author.slug ? (
+
+ {children}
+
+ ) : (
+
{children}
+ );
+}
+
+function postCategory(category) {
+ return (
+
+ {category.title}
+
+ );
+}
diff --git a/src/layouts/index.ts b/src/layouts/index.ts
index 595f1bd3..86ea0d08 100644
--- a/src/layouts/index.ts
+++ b/src/layouts/index.ts
@@ -1,4 +1,6 @@
import PageLayout from './PageLayout';
+import PostFeedLayout from './PostFeedLayout';
+import PostFeedCategoryLayout from './PostFeedCategoryLayout';
import PostLayout from './PostLayout';
-export { PageLayout, PostLayout };
+export { PageLayout, PostFeedLayout, PostFeedCategoryLayout, PostLayout };
\ No newline at end of file
diff --git a/src/utils/get-data-attrs.ts b/src/utils/get-data-attrs.ts
new file mode 100644
index 00000000..6f039074
--- /dev/null
+++ b/src/utils/get-data-attrs.ts
@@ -0,0 +1,8 @@
+export function getDataAttrs(props: any = {}): any {
+ return Object.entries(props).reduce((dataAttrs, [key, value]) => {
+ if (key.startsWith('data-')) {
+ dataAttrs[key] = value;
+ }
+ return dataAttrs;
+ }, {});
+}
diff --git a/stackbit.yaml b/stackbit.yaml
index abce2ced..88d65f17 100644
--- a/stackbit.yaml
+++ b/stackbit.yaml
@@ -12,6 +12,10 @@ contentModels:
isPage: true
Person:
isPage: false
+ PostFeedLayout:
+ isPage: true
+ PostFeedCategoryLayout:
+ isPage: true
modelsSource:
type: files