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.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 (
+
+
+
+
+
+
+ {rootLink}
+
+ {'>'}
+
+ {post.title}
+
+
+
+ {post.title}
+
+
{post.subtitle}
+
{post.date}
+
+
+
+
+
+
+ {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.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 (
-
-
-
+
);
})}
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',
+}