diff --git a/marketing/components/modules/Blog/Blog/Blog.module.scss b/marketing/components/modules/Blog/Blog/Blog.module.scss new file mode 100644 index 00000000..fa4d3c68 --- /dev/null +++ b/marketing/components/modules/Blog/Blog/Blog.module.scss @@ -0,0 +1,38 @@ +@use 'styles/core/boilerplate' as *; + +.wrapper { + display: flex; + justify-content: center; + padding: 0 18px; +} + +.container { + width: 100%; + max-width: $container-max-width; +} + +.header { + padding: 60px 0 40px; + border-bottom: 2px solid $color-background-overlay; + + @include tablet-down { + padding: 40px 0 20px; + } +} + +.preview { + &_wrapper { + @include tablet-down { + display: flex; + justify-content: center; + } + } + &_container { + @include tablet-down { + display: flex; + flex-wrap: wrap; + gap: 40px; + max-width: 540px; + } + } +} diff --git a/marketing/components/modules/Blog/Blog/Blog.tsx b/marketing/components/modules/Blog/Blog/Blog.tsx new file mode 100644 index 00000000..ff085df9 --- /dev/null +++ b/marketing/components/modules/Blog/Blog/Blog.tsx @@ -0,0 +1,27 @@ +import styles from './Blog.module.scss'; +import BlogPreview from '../BlogPreview/BlogPreview'; +import { BlogT } from 'types'; + +type BlogPropsT = { + blogData: BlogT; +}; + +const Blog = ({ blogData }: BlogPropsT) => ( +
+
+
+

{blogData.name}

+
+ +
+
+ {blogData.posts.map((post) => ( + + ))} +
+
+
+
+); + +export default Blog; diff --git a/marketing/components/modules/Blog/BlogPost/BlogPost.module.scss b/marketing/components/modules/Blog/BlogPost/BlogPost.module.scss new file mode 100644 index 00000000..b2032a96 --- /dev/null +++ b/marketing/components/modules/Blog/BlogPost/BlogPost.module.scss @@ -0,0 +1,55 @@ +@use 'styles/core/boilerplate' as *; + +.wrapper { + display: flex; + justify-content: center; +} + +.header { + a { + @include primary-link; + @include body-sm; + } +} + +.bread_crumb { + margin-bottom: 24px; + display: flex; + align-items: center; +} + +.container { + width: 100%; + max-width: $container-max-width; + padding: 40px 18px; + display: flex; + flex-direction: column; + gap: 18px; +} + +.attributes { + display: flex; + justify-content: space-between; + @include body-md-semi-bold; + @include tablet-down { + flex-direction: column; + gap: 8px; + } +} + +.post { + @include body-md(); + + & > p { + margin-bottom: 12px; + text-indent: 40px; + } +} + +.back_button { + margin-right: 18px; + + @include tablet-down { + display: none; + } +} diff --git a/marketing/components/modules/Blog/BlogPost/BlogPost.tsx b/marketing/components/modules/Blog/BlogPost/BlogPost.tsx new file mode 100644 index 00000000..5bcd3b0d --- /dev/null +++ b/marketing/components/modules/Blog/BlogPost/BlogPost.tsx @@ -0,0 +1,66 @@ +import { BlogPostT } from 'types'; +import styles from './BlogPost.module.scss'; +import Bar from '@Shared/Bar/Bar'; +import Image from 'next/image'; +import Link from 'next/link'; +import { RoutesE } from 'types/Routes'; +import { useRouter } from 'next/router'; +import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; +import { Button } from '@mozilla/lilypad-ui'; +type BlogPostPropsT = { + rootLink?: string; + post: BlogPostT; +}; + +const BlogPost = ({ post, rootLink = 'Blog' }: BlogPostPropsT) => { + const router = useRouter(); + const onBackClick = () => { + router.push(RoutesE.BLOG); + }; + + return ( +
+
+
+
+
+ +

{post.title}

+
+

{post.subtitle}

+

{post.date}

+
+
+ +
+ {post.imageAlt} +
+ +
+ {documentToReactComponents(post.post.json)} +
+
+
+ ); +}; + +export default BlogPost; diff --git a/marketing/components/modules/Blog/BlogPreview/BlogPreview.module.scss b/marketing/components/modules/Blog/BlogPreview/BlogPreview.module.scss new file mode 100644 index 00000000..0f179309 --- /dev/null +++ b/marketing/components/modules/Blog/BlogPreview/BlogPreview.module.scss @@ -0,0 +1,88 @@ +@use 'styles/core/boilerplate' as *; + +.wrapper { + display: flex; + justify-content: center; + + @include tablet-down { + width: 250px; + } + + @include mobile-down { + width: 100%; + } +} + +.container { + background: transparent; + text-align: left; + border: 0; + margin-top: 20px; + width: 100%; + max-width: $container-max-width; + display: flex; + transition: 0.5s ease; + + @include tablet-down { + flex-direction: column; + } + + &:hover { + cursor: pointer; + @include tablet-up { + color: $color-interaction-primary; + } + + .image { + @include tablet-up { + transform: scale(1.03); + transition: 0.5s ease; + } + } + + .content { + @include tablet-up { + margin-left: 15px; + } + } + } +} + +.content { + margin: 20px 20px 20px 0; + display: flex; + flex-direction: column; + gap: 12px; + justify-content: center; + transition: 0.5s ease; + + @include tablet-down { + gap: 8px; + } +} + +.image { + flex-shrink: 0; + width: 250px; + height: 250px; + transition: 0.5s ease; + + @include tablet-up { + margin: 20px 20px 20px 0; + } + + @include mobile-down { + flex-shrink: 1; + height: 100%; + width: 100%; + } +} + +.cta { + display: none; + @include tablet-down { + display: initial; + text-align: center; + margin-top: 20px; + } +} diff --git a/marketing/components/modules/Blog/BlogPreview/BlogPreview.tsx b/marketing/components/modules/Blog/BlogPreview/BlogPreview.tsx new file mode 100644 index 00000000..7accd115 --- /dev/null +++ b/marketing/components/modules/Blog/BlogPreview/BlogPreview.tsx @@ -0,0 +1,43 @@ +import styles from './BlogPreview.module.scss'; +import { BlogPostPreviewT } from 'types'; +import Image from 'next/image'; +import Link from 'next/link'; +import { Button } from '@mozilla/lilypad-ui'; + +type BlogPreviewPropsT = { + post: BlogPostPreviewT; +}; + +const BlogPreview = ({ post }: BlogPreviewPropsT) => ( + +
+
+
+ {post.imageAlt} +
+
+

{post.title}

+

{post.subtitle}

+

{post.date}

+

{post.preview}

+
+
+
+
+
+ +); + +export default BlogPreview; diff --git a/marketing/components/modules/Blog/index.ts b/marketing/components/modules/Blog/index.ts new file mode 100644 index 00000000..269fe940 --- /dev/null +++ b/marketing/components/modules/Blog/index.ts @@ -0,0 +1 @@ +export { default } from './Blog/Blog'; diff --git a/marketing/components/shared/MobileCarousel/MobileCarousel.module.scss b/marketing/components/shared/MobileCarousel/MobileCarousel.module.scss deleted file mode 100644 index 65071dc3..00000000 --- a/marketing/components/shared/MobileCarousel/MobileCarousel.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.image { - margin-left: 3%; -} diff --git a/marketing/components/shared/MobileCarousel/MobileCarousel.tsx b/marketing/components/shared/MobileCarousel/MobileCarousel.tsx index e1ed3fb9..26ebb47e 100644 --- a/marketing/components/shared/MobileCarousel/MobileCarousel.tsx +++ b/marketing/components/shared/MobileCarousel/MobileCarousel.tsx @@ -1,4 +1,3 @@ -import styles from './MobileCarousel.module.scss'; import { Swiper, SwiperSlide } from 'swiper/react'; import Image, { StaticImageData } from 'next/image'; import 'swiper/css'; @@ -14,14 +13,17 @@ type MobileCarouselPropsT = { const MobileCarousel = ({ slides }: MobileCarouselPropsT) => { return ( -
- +
+ {slides.map(({ src, alt }) => { return ( -
- {alt} -
+ {alt}
); })} diff --git a/marketing/pages/blog/[slug].tsx b/marketing/pages/blog/[slug].tsx new file mode 100644 index 00000000..b13529a2 --- /dev/null +++ b/marketing/pages/blog/[slug].tsx @@ -0,0 +1,65 @@ +import { BlogPostT, GetStaticPropsT } from 'types'; +import Head from 'next/head'; +import { NavigationT } from 'types'; +import LayoutWrapper from 'layouts/LayoutWrapper/LayoutWrapper'; +import BlogPost from '@Modules/Blog/BlogPost/BlogPost'; +import { + getStaticPathEntries, + getBlogPageData, +} from 'services/contentful.service'; + +type PagePropsT = { + navigation: NavigationT; + post: BlogPostT; +}; + +const Page = ({ navigation, post }: PagePropsT) => { + return ( + +
+ + {post.title} + +
+ +
+
+
+ ); +}; + +export default Page; + +export async function getStaticProps({ params }: GetStaticPropsT) { + try { + const { navigation, post } = await getBlogPageData(params.slug); + + return { + props: { + navigation, + post, + }, + }; + } catch (error) { + console.error('ERROR', error); + return { + props: { + navData: {}, + }, + }; + } +} + +export async function getStaticPaths() { + // Get Entries + const entries = await getStaticPathEntries('blogPost'); + // Create Paths Object + const paths = entries.items.map((item) => { + return { params: { slug: item.fields.slug } }; + }); + + return { + paths: paths, + fallback: 'blocking', + }; +} diff --git a/marketing/pages/blog/index.tsx b/marketing/pages/blog/index.tsx new file mode 100644 index 00000000..0a22bd36 --- /dev/null +++ b/marketing/pages/blog/index.tsx @@ -0,0 +1,57 @@ +import Head from 'next/head'; +import { BlogT, NavigationT } from 'types'; +import LayoutWrapper from 'layouts/LayoutWrapper/LayoutWrapper'; +import { + getBlogData, + getNavigationLinksEntry, +} from 'services/contentful.service'; +import Blog from '@Modules/Blog'; + +type HomePropsT = { + navData: NavigationT; + blogData: BlogT; +}; + +const Page = ({ navData, blogData }: HomePropsT) => { + return ( + +
+ + {blogData ? blogData.name : 'Hubs Blog'} + +
+ {blogData ? ( + + ) : ( +
+ There was a problem loading the blog +
+ )} +
+
+
+ ); +}; + +export default Page; + +export async function getStaticProps() { + try { + const blogData = await getBlogData(); + const navData = await getNavigationLinksEntry(); + return { + props: { + navData, + blogData, + }, + }; + } catch (error) { + console.error('ERROR', error); + return { + props: { + navData: {}, + blogData: {}, + }, + }; + } +} diff --git a/marketing/pages/index.tsx b/marketing/pages/index.tsx index 4567efed..f8c3f1bf 100644 --- a/marketing/pages/index.tsx +++ b/marketing/pages/index.tsx @@ -40,12 +40,8 @@ export default Home; export async function getStaticProps() { try { - const sectionsData = await getSectionsData( - 'homePage', - 'iUw7LHBaBcgGaKydU2qKJ' - ); - - const navData = await getNavigationLinksEntry('4FsGf6XPSDTPppGDlyFYm9'); + const sectionsData = await getSectionsData(); + const navData = await getNavigationLinksEntry(); return { props: { diff --git a/marketing/services/contentful.service.ts b/marketing/services/contentful.service.ts index 1f0178b3..b10f7228 100644 --- a/marketing/services/contentful.service.ts +++ b/marketing/services/contentful.service.ts @@ -1,12 +1,21 @@ import axios, { AxiosResponse } from 'axios'; import { createClient } from 'contentful'; -import { NavigationT, HeroT, CustomSectionsT, PathCollectionT } from 'types'; +import { + NavigationT, + HeroT, + CustomSectionsT, + PathCollectionT, + BlogT, + BlogPageT, +} from 'types'; import { Entry, EntryCollection } from 'contentful'; import { createNavigationQuery, createSectionsQuery, createCustomPageQuery, -} from './queries'; + createBlogQuery, + createBlogPageQuery, +} from './queries.service'; const CONTENTFUL_ENV = process.env.ENV === 'prod' ? 'master' : 'development'; const SPACE = 'p5qj0ed8ji31'; @@ -69,14 +78,11 @@ export const getStaticPathEntries = async ( /** * Get Navigation Content - * @param id * @returns NavigationT[] */ -export const getNavigationLinksEntry = async ( - id: string -): Promise => { +export const getNavigationLinksEntry = async (): Promise => { const { data, statusText } = await axios - .post(URL, { query: createNavigationQuery(id) }, { ...PROTOCOLS }) + .post(URL, { query: `{${createNavigationQuery()} }` }, { ...PROTOCOLS }) .then(({ data }: AxiosResponse) => data); // Query is wrong @@ -95,16 +101,12 @@ export const getNavigationLinksEntry = async ( /** * Get Sections Data - * @param name - * @param id - * @returns + * @returns CustomSectionsT */ -export const getSectionsData = async ( - name: string, - id: string -): Promise => { +export const getSectionsData = async (): Promise => { + const name = 'homePage'; const { data, statusText } = await axios - .post(URL, { query: createSectionsQuery(name, id) }, { ...PROTOCOLS }) + .post(URL, { query: createSectionsQuery(name) }, { ...PROTOCOLS }) .then(({ data }: AxiosResponse) => data); // Query is wrong @@ -114,10 +116,34 @@ export const getSectionsData = async ( return data[name].sectionsCollection; }; +/** + * Get Blog Data + * @returns BlogT + */ +export const getBlogData = async (): Promise => { + const name = 'blog'; + + const { data, statusText } = await axios + .post(URL, { query: createBlogQuery(name) }, { ...PROTOCOLS }) + .then(({ data }: AxiosResponse) => data); + + // Query is wrong + if (handleBadRequest(statusText)) { + throw statusText; + } + + const { name: blogName, blogPostCollection: post } = data[name]; + const blog: BlogT = { + name: blogName, + posts: post.items, + }; + return blog; +}; + /** * Get Custom Page Data * @param slug - * @returns + * @returns CustomSectionsT */ export const getCustomPageData = async ( slug: string @@ -133,3 +159,32 @@ export const getCustomPageData = async ( return data.customPageCollection.items[0].sectionsCollection; }; + +/** + * Get Custom Page Data + * @param slug + * @returns BlogPageT + */ +export const getBlogPageData = async (slug: string): Promise => { + const { data, statusText } = await axios + .post(URL, { query: createBlogPageQuery(slug) }, { ...PROTOCOLS }) + .then(({ data }: AxiosResponse) => data); + + // Query is wrong + if (handleBadRequest(statusText)) { + throw statusText; + } + + const { linksCollection, bannerText, bannerIcon } = data.navigation; + + const blogPageData: BlogPageT = { + navigation: { + bannerText, + bannerIcon, + links: linksCollection.items, + }, + post: data.blogPostCollection.items[0], + }; + + return blogPageData; +}; diff --git a/marketing/services/queries.ts b/marketing/services/queries.service.ts similarity index 60% rename from marketing/services/queries.ts rename to marketing/services/queries.service.ts index 82a0f290..2f5be7ef 100644 --- a/marketing/services/queries.ts +++ b/marketing/services/queries.service.ts @@ -3,23 +3,11 @@ * @param id * @returns */ -export const createNavigationQuery = (id: string) => { - return `{ - navigation(id: "${id}") { - linksCollection { - items { - ... on Link { - href - label - text - } - } - } - bannerText - bannerIcon - } - } -`; +export const createNavigationQuery = () => { + const id = '4FsGf6XPSDTPppGDlyFYm9'; + return `navigation(id: "${id}") { + ${navCollection} + }`; }; /** @@ -41,19 +29,98 @@ export const createCustomPageQuery = (slug: string) => { `; }; +/** + * Create Blog Page Query + * @param slug + * @returns + */ +export const createBlogPageQuery = (slug: string) => { + return `{ + ${createNavigationQuery()} + blogPostCollection(limit:1,where:{slug:"${slug}"}){ + items { + ... on BlogPost { + ${blogCollection} + post { + json + } + featuredImage { + url + } + } + } + } + }`; +}; + +/** + * Create Blog Query + * @param name + * @returns query + */ +export const createBlogQuery = (name: string) => { + const id = '4NssSFRY8TUWetnJjH9gwF'; + + return `query {${name}(id: "${id}") { + name + blogPostCollection { + items { + ... on BlogPost { + ${blogCollection} + thumbnailImage { + url (transform: { + width: 800, + height: 800, + resizeStrategy: FILL, + resizeFocus: CENTER, + cornerRadius: 20, + }) + } + } + } + } + }}`; +}; + /** * Create Section Collection Query * @param name - * @param id * @returns query */ -export const createSectionsQuery = (name: string, id: string) => { +export const createSectionsQuery = (name: string) => { + const id = 'iUw7LHBaBcgGaKydU2qKJ'; + return `query {${name}(id: "${id}") { ${sectionsCollection} }}`; }; /** * Query Bank */ +const blogCollection = ` +sys { + id +} +slug +title +subtitle +date +preview +imageAlt +`; + +const navCollection = ` +linksCollection { + items { + ... on Link { + href + label + text + } + } +} +bannerText +bannerIcon`; + const sectionsCollection = ` sectionsCollection { items { diff --git a/marketing/styles/tools/mixins.scss b/marketing/styles/tools/mixins.scss index 30475be4..5f98d64f 100644 --- a/marketing/styles/tools/mixins.scss +++ b/marketing/styles/tools/mixins.scss @@ -10,7 +10,7 @@ font-size: rem($size); } -@mixin primary-font($weight: 600) { +@mixin primary-font($weight: 500) { font-family: $primary-font, $backup-font; font-weight: $weight; } @@ -131,10 +131,24 @@ } } +@mixin body-md-semi-bold { + @include primary-font(600); + @include font-size(16); + line-height: 24px; + + @include tablet-down { + @include font-size(14); + } +} + @mixin body-md-bold { @include primary-font(700); @include font-size(16); line-height: 24px; + + @include tablet-down { + @include font-size(14); + } } @mixin body-sm { diff --git a/marketing/types.d.ts b/marketing/types.d.ts index cae09934..1419ef2f 100644 --- a/marketing/types.d.ts +++ b/marketing/types.d.ts @@ -5,10 +5,39 @@ import { ButtonCategoriesT } from '@mozilla/lilypad-ui'; * CONTENFUL MEDIA TYPES */ export type CustomSectionsT = { - //Expoand the item type as we add more custom sections + //Expand the item type as we add more custom sections items: TitleDescriptionT[] | FiftyfiftyT[] | HeroT[] | TileSpotlightT[]; }; +export type BlogPageT = { + navigation: NavigationT; + post: BlogPostT; +}; + +export type BlogT = { + name: string; + posts: BlogPostPreviewT[]; +}; + +export type BlogPostPreviewT = Omit; + +export type BlogPostT = { + sys: { + id: string; + }; + slug: string; + title: string; + subtitle: string; + date: string; + featuredImage: ImageT; + thumbnailImage: ImageT; + imageAlt: string; + preview: string; + post: { + json: Document; + }; +}; + export type FiftyfiftyT = { adornment: 'swoosh' | 'none'; desktopImage: ImageT; @@ -102,3 +131,9 @@ export type NewContactT = { }; type PlansT = 'starter' | 'personal' | 'professional' | 'business' | null; + +export type GetStaticPropsT = { + params: { + slug: string; + }; +}; diff --git a/marketing/types/Routes.ts b/marketing/types/Routes.ts new file mode 100644 index 00000000..fd8dc675 --- /dev/null +++ b/marketing/types/Routes.ts @@ -0,0 +1,3 @@ +export enum RoutesE { + BLOG = '/blog', +}