From c0abd8dfcf7862aab8341d7fd29897313207d058 Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Wed, 17 Nov 2021 23:50:08 +0200 Subject: [PATCH 01/10] Added PostFeedLayout and PostCategoryLayout --- models/PostCategoryLayout.yaml | 6 +++ models/PostFeedLayout.yaml | 40 ++++++++++++++++++++ src/components-manifest.json | 10 +++++ src/layouts/PostCategoryLayout/index.tsx | 2 + src/layouts/PostFeedLayout/index.tsx | 48 ++++++++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 models/PostCategoryLayout.yaml create mode 100644 models/PostFeedLayout.yaml create mode 100644 src/layouts/PostCategoryLayout/index.tsx create mode 100644 src/layouts/PostFeedLayout/index.tsx diff --git a/models/PostCategoryLayout.yaml b/models/PostCategoryLayout.yaml new file mode 100644 index 00000000..2a71eada --- /dev/null +++ b/models/PostCategoryLayout.yaml @@ -0,0 +1,6 @@ +name: PostCategoryLayout +label: Blog Category +layout: PostCategoryLayout +hideContent: true +extends: + - PostFeedLayout diff --git a/models/PostFeedLayout.yaml b/models/PostFeedLayout.yaml new file mode 100644 index 00000000..447c305a --- /dev/null +++ b/models/PostFeedLayout.yaml @@ -0,0 +1,40 @@ +name: PostFeedLayout +label: Blog +layout: PostFeedLayout +hideContent: true +fields: + - type: string + name: title + label: Title + default: This is a page title + required: true + - type: number + name: numOfPostsPerPage + label: Number of Posts per page + default: 10 + - type: enum + name: variant + group: styles + label: Arrangement + options: + - label: Three columns grid + value: variant-a + - label: List + value: variant-b + default: variant-a + - name: colors + default: colors-h + - 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/src/components-manifest.json b/src/components-manifest.json index 60493634..cb70f7d1 100644 --- a/src/components-manifest.json +++ b/src/components-manifest.json @@ -133,5 +133,15 @@ "path": "layouts/PostLayout", "modelName": "PostLayout", "isDynamic": true + }, + "PostFeedLayout": { + "path": "layouts/PostFeedLayout", + "modelName": "PostFeedLayout", + "isDynamic": true + }, + "PostCategoryLayout": { + "path": "layouts/PostCategoryLayout", + "modelName": "PostCategoryLayout", + "isDynamic": true } } diff --git a/src/layouts/PostCategoryLayout/index.tsx b/src/layouts/PostCategoryLayout/index.tsx new file mode 100644 index 00000000..4e6b6605 --- /dev/null +++ b/src/layouts/PostCategoryLayout/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..74c1436e --- /dev/null +++ b/src/layouts/PostFeedLayout/index.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +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, numOfPages, numOfTotalItems, ...rest } = page; + const PostFeedSection = getComponent('PostFeedSection'); + + 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 ( +
+ +
+ ); + })} +
+ ); +} From 2560fabab8b37b1a407d69c68b43f8ba3810b3f2 Mon Sep 17 00:00:00 2001 From: tomasbankauskas Date: Thu, 18 Nov 2021 18:02:41 +0200 Subject: [PATCH 02/10] updated PostLayout component and model --- models/PostLayout.yaml | 5 +++++ src/layouts/PostFeedLayout/index.tsx | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/models/PostLayout.yaml b/models/PostLayout.yaml index 63012c51..bf4a5448 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: + - PostCategoryLayout - type: reference name: author label: Author diff --git a/src/layouts/PostFeedLayout/index.tsx b/src/layouts/PostFeedLayout/index.tsx index 74c1436e..0afd1c94 100644 --- a/src/layouts/PostFeedLayout/index.tsx +++ b/src/layouts/PostFeedLayout/index.tsx @@ -1,13 +1,23 @@ import * as React from 'react'; +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, numOfPages, numOfTotalItems, ...rest } = page; + const { title, topSections = [], bottomSections = [], pageIndex, baseUrlPath, numOfPages, numOfTotalItems, ...rest } = page; const PostFeedSection = getComponent('PostFeedSection'); + // blog/category/react + const pageLinks = []; + for (let i = 0; i < numOfPages; i++) { + const urlPath = i === 0 ? baseUrlPath : `${baseUrlPath}/page/${i + 1}`; + pageLinks.push( + Page {i + 1} + ); + } + return (
@@ -17,9 +27,9 @@ export default function PostFeedLayout(props) { )} {renderSections(topSections, 'topSections')} -
+ -
+
{pageLinks}
{renderSections(bottomSections, 'bottomSections')}
From 1c78568ac12614a3f3c9a2a356806deb2e6b6be9 Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Thu, 18 Nov 2021 23:02:15 +0200 Subject: [PATCH 03/10] Improved page links in PostFeedLayout --- src/layouts/PostFeedLayout/index.tsx | 96 ++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/src/layouts/PostFeedLayout/index.tsx b/src/layouts/PostFeedLayout/index.tsx index 0afd1c94..75e2cdcf 100644 --- a/src/layouts/PostFeedLayout/index.tsx +++ b/src/layouts/PostFeedLayout/index.tsx @@ -9,15 +9,6 @@ export default function PostFeedLayout(props) { const { title, topSections = [], bottomSections = [], pageIndex, baseUrlPath, numOfPages, numOfTotalItems, ...rest } = page; const PostFeedSection = getComponent('PostFeedSection'); - // blog/category/react - const pageLinks = []; - for (let i = 0; i < numOfPages; i++) { - const urlPath = i === 0 ? baseUrlPath : `${baseUrlPath}/page/${i + 1}`; - pageLinks.push( - Page {i + 1} - ); - } - return (
@@ -27,9 +18,8 @@ export default function PostFeedLayout(props) { )} {renderSections(topSections, 'topSections')} - - -
{pageLinks}
+ + {renderSections(bottomSections, 'bottomSections')}
@@ -56,3 +46,85 @@ function renderSections(sections: any[], fieldName: string) { ); } + +function PageLinks({ pageIndex, baseUrlPath, numOfPages, numOfTotalItems }) { + 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}`; +} From 7cd9066b483b7d38cf1f45745f629c685bc6b7f8 Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Wed, 24 Nov 2021 22:15:04 +0200 Subject: [PATCH 04/10] Fixed PostFeedLayout and PostFeedSection renamed PostCategoryLayout to PostFeedCategoryLayout --- ...goryLayout.yaml => PostFeedCategoryLayout.yaml} | 4 ++-- src/components-manifest.json | 6 +++--- src/components/PostFeedSection/index.tsx | 14 ++++++++++---- .../index.tsx | 0 src/layouts/PostFeedLayout/index.tsx | 14 ++++++++------ 5 files changed, 23 insertions(+), 15 deletions(-) rename models/{PostCategoryLayout.yaml => PostFeedCategoryLayout.yaml} (52%) rename src/layouts/{PostCategoryLayout => PostFeedCategoryLayout}/index.tsx (100%) diff --git a/models/PostCategoryLayout.yaml b/models/PostFeedCategoryLayout.yaml similarity index 52% rename from models/PostCategoryLayout.yaml rename to models/PostFeedCategoryLayout.yaml index 2a71eada..67c872dd 100644 --- a/models/PostCategoryLayout.yaml +++ b/models/PostFeedCategoryLayout.yaml @@ -1,6 +1,6 @@ -name: PostCategoryLayout +name: PostFeedCategoryLayout label: Blog Category -layout: PostCategoryLayout +layout: PostFeedCategoryLayout hideContent: true extends: - PostFeedLayout diff --git a/src/components-manifest.json b/src/components-manifest.json index cb70f7d1..429ef136 100644 --- a/src/components-manifest.json +++ b/src/components-manifest.json @@ -139,9 +139,9 @@ "modelName": "PostFeedLayout", "isDynamic": true }, - "PostCategoryLayout": { - "path": "layouts/PostCategoryLayout", - "modelName": "PostCategoryLayout", + "PostFeedCategoryLayout": { + "path": "layouts/PostFeedCategoryLayout", + "modelName": "PostFeedCategoryLayout", "isDynamic": true } } diff --git a/src/components/PostFeedSection/index.tsx b/src/components/PostFeedSection/index.tsx index ec46952d..fd00c38a 100644 --- a/src/components/PostFeedSection/index.tsx +++ b/src/components/PostFeedSection/index.tsx @@ -17,7 +17,7 @@ export default function PostFeedSection(props) { className={classNames( 'sb-component', 'sb-component-section', - 'sb-component-latest-posts-section', + 'sb-component-posts-feed-section', colors, 'flex', 'flex-col', @@ -25,7 +25,7 @@ export default function PostFeedSection(props) { 'relative', sectionStyles.height ? mapMinHeightStyles(sectionStyles.height) : null, sectionStyles.margin, - sectionStyles.padding, + sectionStyles.padding || 'py-12 px-4', sectionStyles.borderColor, sectionStyles.borderRadius ? mapStyles({ borderRadius: sectionStyles.borderRadius }) : null, sectionStyles.borderStyle ? mapStyles({ borderStyle: sectionStyles.borderStyle }) : null @@ -34,8 +34,14 @@ export default function PostFeedSection(props) { borderWidth: `${sectionBorderWidth}px` }} > -
-
+
+
{postFeedHeader(props)} {postFeedVariants(props)} {postFeedActions(props)} diff --git a/src/layouts/PostCategoryLayout/index.tsx b/src/layouts/PostFeedCategoryLayout/index.tsx similarity index 100% rename from src/layouts/PostCategoryLayout/index.tsx rename to src/layouts/PostFeedCategoryLayout/index.tsx diff --git a/src/layouts/PostFeedLayout/index.tsx b/src/layouts/PostFeedLayout/index.tsx index 75e2cdcf..82300f6e 100644 --- a/src/layouts/PostFeedLayout/index.tsx +++ b/src/layouts/PostFeedLayout/index.tsx @@ -2,24 +2,26 @@ import * as React from 'react'; import Link from '../../utils/link'; import { getComponent } from '../../components-registry'; import { getBaseLayoutComponent } from '../../utils/base-layout'; +import classNames from 'classnames'; export default function PostFeedLayout(props) { const { page, site } = props; const BaseLayout = getBaseLayoutComponent(page.baseLayout, site.baseLayout); const { title, topSections = [], bottomSections = [], pageIndex, baseUrlPath, numOfPages, numOfTotalItems, ...rest } = page; const PostFeedSection = getComponent('PostFeedSection'); + const colors = page.colors || 'colors-a'; return (
{title && ( -

+

{title}

)} {renderSections(topSections, 'topSections')} - + {renderSections(bottomSections, 'bottomSections')}
@@ -47,7 +49,7 @@ function renderSections(sections: any[], fieldName: string) { ); } -function PageLinks({ pageIndex, baseUrlPath, numOfPages, numOfTotalItems }) { +function PageLinks({ pageIndex, baseUrlPath, numOfPages, numOfTotalItems, colors }) { if (numOfPages < 2) { return null; } @@ -102,12 +104,12 @@ function PageLinks({ pageIndex, baseUrlPath, numOfPages, numOfTotalItems }) { pageLinks.push(); } - return
    {pageLinks}
; + return
    {pageLinks}
; } function PageLink({ pageIndex, buttonLabel, baseUrlPath }) { return ( - + {buttonLabel} ); @@ -115,7 +117,7 @@ function PageLink({ pageIndex, buttonLabel, baseUrlPath }) { function PageLinkDisabled({ buttonLabel }) { return ( - + {buttonLabel} ); From 5300c7e5ceb8b337d9bbe0cf7b0339d6c69a2f3a Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Wed, 24 Nov 2021 22:48:29 +0200 Subject: [PATCH 05/10] updated PostFeedLayout model --- models/PostFeedLayout.yaml | 68 +++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/models/PostFeedLayout.yaml b/models/PostFeedLayout.yaml index 447c305a..8bc65148 100644 --- a/models/PostFeedLayout.yaml +++ b/models/PostFeedLayout.yaml @@ -1,7 +1,11 @@ name: PostFeedLayout label: Blog +labelField: title layout: PostFeedLayout hideContent: true +fieldGroups: + - name: styles + label: Styles fields: - type: string name: title @@ -11,6 +15,7 @@ fields: - 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: enum name: variant @@ -22,7 +27,58 @@ fields: - label: List value: variant-b default: variant-a - - name: colors + - type: enum + name: colors + label: Colors + description: The color theme of the section + group: styles + controlType: palette + options: + - label: Colors A + value: colors-a + textColor: '$onLight' + backgroundColor: '$light' + borderColor: '#ececec' + - label: Colors B + value: colors-b + textColor: '$primary' + backgroundColor: '$light' + borderColor: '#ececec' + - label: Colors C + value: colors-c + textColor: '$onDark' + backgroundColor: '$dark' + borderColor: '#ececec' + - label: Colors D + value: colors-d + textColor: '$primary' + backgroundColor: '$dark' + borderColor: '#ececec' + - label: Colors E + value: colors-e + textColor: '$onPrimary' + backgroundColor: '$primary' + borderColor: '#ececec' + - label: Colors F + value: colors-f + textColor: '$onSecondary' + backgroundColor: '$secondary' + borderColor: '#ececec' + - label: Colors G + value: colors-g + textColor: '$primary' + backgroundColor: '$secondary' + borderColor: '#ececec' + - label: Colors H + value: colors-h + textColor: '$onComplementary' + backgroundColor: '$complementary' + borderColor: '#ececec' + - label: Colors I + value: colors-i + textColor: '$onComplementaryAlt' + backgroundColor: '$complementaryAlt' + borderColor: '#ececec' default: colors-h - type: list name: topSections @@ -38,3 +94,13 @@ fields: type: model groups: - sectionComponent + - type: style + name: styles + styles: + self: + margin: ['tw0:36'] + padding: ['tw4:36'] + title: + fontWeight: ['400', '700'] + fontStyle: ['normal', 'italic'] + textAlign: ['left', 'center', 'right'] From 30f83dfd46fc715ea3a755289f1bb0cc8ff4e2de Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Thu, 25 Nov 2021 00:16:14 +0200 Subject: [PATCH 06/10] fixed PostLayout model --- models/PostLayout.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/PostLayout.yaml b/models/PostLayout.yaml index bf4a5448..d0d9357e 100644 --- a/models/PostLayout.yaml +++ b/models/PostLayout.yaml @@ -16,7 +16,7 @@ fields: name: category label: Category models: - - PostCategoryLayout + - PostFeedCategoryLayout - type: reference name: author label: Author From dd9ee841da1ce11371b1c6856f392db83cb7bedc Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Thu, 25 Nov 2021 23:34:02 +0200 Subject: [PATCH 07/10] Consolidated all post related sections into PostFeedSection --- models/FeaturedPostsSection.yaml | 112 +----- models/PagedPostsSection.yaml | 26 ++ models/Person.yaml | 9 +- models/PostFeedLayout.yaml | 81 +---- models/PostFeedSection.yaml | 46 +-- models/RecentPostsSection.yaml | 26 ++ src/components-manifest.json | 15 +- src/components/FeaturedPostsSection/index.tsx | 335 +----------------- src/components/PostFeedSection/index.tsx | 273 +++++++++++--- src/components/RecentPostsSection/index.tsx | 2 + src/layouts/PostFeedLayout/index.tsx | 13 +- src/layouts/PostLayout/index.tsx | 54 ++- stackbit.yaml | 4 + 13 files changed, 382 insertions(+), 614 deletions(-) create mode 100644 models/PagedPostsSection.yaml create mode 100644 models/RecentPostsSection.yaml create mode 100644 src/components/RecentPostsSection/index.tsx 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..d146e6e7 --- /dev/null +++ b/models/PagedPostsSection.yaml @@ -0,0 +1,26 @@ +type: object +name: PagedPostsSection +label: Post feed +labelField: title +extends: + - PostFeedSection +fields: + - name: title + hidden: true + - name: subtitle + hidden: true + - 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 + - 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/PostFeedLayout.yaml b/models/PostFeedLayout.yaml index 8bc65148..bbba0db6 100644 --- a/models/PostFeedLayout.yaml +++ b/models/PostFeedLayout.yaml @@ -3,83 +3,20 @@ label: Blog labelField: title layout: PostFeedLayout hideContent: true -fieldGroups: - - name: styles - label: Styles fields: - type: string name: title label: Title default: This is a page title - required: true - 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: enum - name: variant - group: styles - label: Arrangement - options: - - label: Three columns grid - value: variant-a - - label: List - value: variant-b - default: variant-a - - type: enum - name: colors - label: Colors - description: The color theme of the section - group: styles - controlType: palette - options: - - label: Colors A - value: colors-a - textColor: '$onLight' - backgroundColor: '$light' - borderColor: '#ececec' - - label: Colors B - value: colors-b - textColor: '$primary' - backgroundColor: '$light' - borderColor: '#ececec' - - label: Colors C - value: colors-c - textColor: '$onDark' - backgroundColor: '$dark' - borderColor: '#ececec' - - label: Colors D - value: colors-d - textColor: '$primary' - backgroundColor: '$dark' - borderColor: '#ececec' - - label: Colors E - value: colors-e - textColor: '$onPrimary' - backgroundColor: '$primary' - borderColor: '#ececec' - - label: Colors F - value: colors-f - textColor: '$onSecondary' - backgroundColor: '$secondary' - borderColor: '#ececec' - - label: Colors G - value: colors-g - textColor: '$primary' - backgroundColor: '$secondary' - borderColor: '#ececec' - - label: Colors H - value: colors-h - textColor: '$onComplementary' - backgroundColor: '$complementary' - borderColor: '#ececec' - - label: Colors I - value: colors-i - textColor: '$onComplementaryAlt' - backgroundColor: '$complementaryAlt' - borderColor: '#ececec' - default: colors-h + - type: model + name: postFeed + label: Post Feed + models: [PagedPostsSection] - type: list name: topSections label: Top Sections @@ -94,13 +31,3 @@ fields: type: model groups: - sectionComponent - - type: style - name: styles - styles: - self: - margin: ['tw0:36'] - padding: ['tw4:36'] - title: - fontWeight: ['400', '700'] - fontStyle: ['normal', 'italic'] - textAlign: ['left', 'center', 'right'] 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/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 429ef136..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", diff --git a/src/components/FeaturedPostsSection/index.tsx b/src/components/FeaturedPostsSection/index.tsx index 1a17e700..4b5e4f7a 100644 --- a/src/components/FeaturedPostsSection/index.tsx +++ b/src/components/FeaturedPostsSection/index.tsx @@ -1,333 +1,2 @@ -import * as React from 'react'; -import classNames from 'classnames'; -import dayjs from 'dayjs'; -import { getComponent } from '../../components-registry'; -import { mapStylesToClassNames as mapStyles } from '../../utils/map-styles-to-class-names'; -import getPageUrlPath from '../../utils/get-page-url-path'; -import Link from '../../utils/link'; - -export default function FeaturedPostsSection(props) { - const cssId = props.elementId || null; - const colors = props.colors || 'colors-a'; - const sectionStyles = props.styles?.self || {}; - const sectionBorderWidth = sectionStyles.borderWidth ? sectionStyles.borderWidth : 0; - 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 ( -
- -
- ); -} - -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/PostFeedSection/index.tsx b/src/components/PostFeedSection/index.tsx index fd00c38a..e834cd38 100644 --- a/src/components/PostFeedSection/index.tsx +++ b/src/components/PostFeedSection/index.tsx @@ -11,19 +11,22 @@ export default function PostFeedSection(props) { const colors = props.colors || 'colors-a'; const sectionStyles = props.styles?.self || {}; const sectionBorderWidth = sectionStyles.borderWidth ? sectionStyles.borderWidth : 0; + const justifyContent = mapStyles({ justifyContent: sectionStyles.justifyContent || 'center' }); + const width = mapMaxWidthStyles(sectionStyles.width || 'wide'); + return (
-
-
+
+
{postFeedHeader(props)} {postFeedVariants(props)} {postFeedActions(props)} + {props.pageLinks}
@@ -101,6 +99,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; } @@ -112,33 +114,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} - -

-
- -
- {post.excerpt &&

{post.excerpt}

} -
-
- ); - })} +
+
+ ))}
); } @@ -150,35 +149,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}

-
- -
- {post.excerpt &&

{post.excerpt}

} + + {post.excerpt && ( +

+ {post.excerpt} +

+ )}

); @@ -187,6 +247,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 ( +
+ +
+ ); +} + +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/RecentPostsSection/index.tsx b/src/components/RecentPostsSection/index.tsx new file mode 100644 index 00000000..4b5e4f7a --- /dev/null +++ b/src/components/RecentPostsSection/index.tsx @@ -0,0 +1,2 @@ +import PostFeedSection from '../PostFeedSection'; +export default PostFeedSection; diff --git a/src/layouts/PostFeedLayout/index.tsx b/src/layouts/PostFeedLayout/index.tsx index 82300f6e..8e396b7e 100644 --- a/src/layouts/PostFeedLayout/index.tsx +++ b/src/layouts/PostFeedLayout/index.tsx @@ -1,15 +1,15 @@ 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'; -import classNames from 'classnames'; export default function PostFeedLayout(props) { const { page, site } = props; const BaseLayout = getBaseLayoutComponent(page.baseLayout, site.baseLayout); - const { title, topSections = [], bottomSections = [], pageIndex, baseUrlPath, numOfPages, numOfTotalItems, ...rest } = page; + const { title, topSections = [], bottomSections = [], pageIndex, baseUrlPath, numOfPages, items, postFeed } = page; const PostFeedSection = getComponent('PostFeedSection'); - const colors = page.colors || 'colors-a'; + const pageLinks = PageLinks({ pageIndex, baseUrlPath, numOfPages }); return ( @@ -20,8 +20,7 @@ export default function PostFeedLayout(props) { )} {renderSections(topSections, 'topSections')} - - + {renderSections(bottomSections, 'bottomSections')} @@ -49,7 +48,7 @@ function renderSections(sections: any[], fieldName: string) { ); } -function PageLinks({ pageIndex, baseUrlPath, numOfPages, numOfTotalItems, colors }) { +function PageLinks({ pageIndex, baseUrlPath, numOfPages }) { if (numOfPages < 2) { return null; } @@ -104,7 +103,7 @@ function PageLinks({ pageIndex, baseUrlPath, numOfPages, numOfTotalItems, colors pageLinks.push(); } - return
    {pageLinks}
; + return
{pageLinks}
; } function PageLink({ pageIndex, buttonLabel, baseUrlPath }) { diff --git a/src/layouts/PostLayout/index.tsx b/src/layouts/PostLayout/index.tsx index 116d1ac3..957f31cb 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 && ( @@ -52,14 +54,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/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 From ccf3b20d75eafa2c9b8a00b184e7fb695b8a0447 Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Fri, 26 Nov 2021 00:36:38 +0200 Subject: [PATCH 08/10] passing data-sb-field-path to section components --- src/components/ContactSection/index.tsx | 2 ++ src/components/CtaSection/index.tsx | 2 ++ src/components/FaqSection/index.tsx | 2 ++ src/components/FeaturedItemsSection/index.tsx | 2 ++ src/components/FeaturedPeopleSection/index.tsx | 2 ++ src/components/HeroSection/index.tsx | 2 ++ src/components/MediaGallerySection/index.tsx | 2 ++ src/components/PostFeedSection/index.tsx | 2 ++ src/components/QuoteSection/index.tsx | 2 ++ src/components/TestimonialsSection/index.tsx | 2 ++ src/components/TextSection/index.tsx | 2 ++ src/layouts/PageLayout/index.tsx | 6 +----- src/layouts/PostFeedLayout/index.tsx | 12 ++++-------- src/layouts/PostLayout/index.tsx | 6 +----- src/utils/get-data-attrs.ts | 8 ++++++++ 15 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 src/utils/get-data-attrs.ts 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 (
- -
- ); + return ; })}
)} diff --git a/src/layouts/PostFeedLayout/index.tsx b/src/layouts/PostFeedLayout/index.tsx index 8e396b7e..99700ee9 100644 --- a/src/layouts/PostFeedLayout/index.tsx +++ b/src/layouts/PostFeedLayout/index.tsx @@ -15,12 +15,12 @@ export default function PostFeedLayout(props) {
{title && ( -

+

{title}

)} {renderSections(topSections, 'topSections')} - + {renderSections(bottomSections, 'bottomSections')}
@@ -32,17 +32,13 @@ function renderSections(sections: any[], fieldName: string) { 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 ( -
- -
- ); + return ; })}
); diff --git a/src/layouts/PostLayout/index.tsx b/src/layouts/PostLayout/index.tsx index 957f31cb..05d1ff86 100644 --- a/src/layouts/PostLayout/index.tsx +++ b/src/layouts/PostLayout/index.tsx @@ -41,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 ; })}
)} 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; + }, {}); +} From 018ea5de35f99232338f269b74d6ae5c7c52571b Mon Sep 17 00:00:00 2001 From: Simon Hanukaev Date: Tue, 30 Nov 2021 21:29:31 +0200 Subject: [PATCH 09/10] updated default values of PagesPostsSection --- models/PagedPostsSection.yaml | 3 +++ models/PostFeedLayout.yaml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/models/PagedPostsSection.yaml b/models/PagedPostsSection.yaml index d146e6e7..3ebdf1f8 100644 --- a/models/PagedPostsSection.yaml +++ b/models/PagedPostsSection.yaml @@ -7,8 +7,10 @@ extends: fields: - name: title hidden: true + default: null - name: subtitle hidden: true + default: null - name: showDate default: true - name: showAuthor @@ -22,5 +24,6 @@ fields: value: variant-d - name: actions hidden: true + default: [] - name: colors default: colors-a diff --git a/models/PostFeedLayout.yaml b/models/PostFeedLayout.yaml index bbba0db6..31467325 100644 --- a/models/PostFeedLayout.yaml +++ b/models/PostFeedLayout.yaml @@ -15,8 +15,17 @@ fields: 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 From 0750814b1e52fbc3c3d75e727c2e95b2cb6a271c Mon Sep 17 00:00:00 2001 From: tomasbankauskas Date: Tue, 30 Nov 2021 22:11:41 +0200 Subject: [PATCH 10/10] updated index.ts in layouts --- src/layouts/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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