commit
526e703d9a
|
@ -0,0 +1,6 @@
|
||||||
|
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
||||||
|
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
||||||
|
|
||||||
|
CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
||||||
|
CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
||||||
|
CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE
|
|
@ -12,11 +12,13 @@
|
||||||
"@ant-design/cssinjs": "^1.18.1",
|
"@ant-design/cssinjs": "^1.18.1",
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@ant-design/nextjs-registry": "^1.0.0",
|
"@ant-design/nextjs-registry": "^1.0.0",
|
||||||
|
"@contentful/rich-text-react-renderer": "^15.22.9",
|
||||||
"agora-rtc-react": "^2.1.0",
|
"agora-rtc-react": "^2.1.0",
|
||||||
"agora-rtc-sdk-ng": "^4.20.2",
|
"agora-rtc-sdk-ng": "^4.20.2",
|
||||||
"antd": "^5.12.1",
|
"antd": "^5.12.1",
|
||||||
"antd-img-crop": "^4.21.0",
|
"antd-img-crop": "^4.21.0",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
|
"contentful": "^10.13.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Bbuddy - Blog item',
|
|
||||||
description: 'Bbuddy desc blog item'
|
|
||||||
};
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return [{ blogId: 'news-1' }, { blogId: 'news-2' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogItem({ params }: { params: { blogId: string } }) {
|
|
||||||
if (!params?.blogId) notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="b-news-page">
|
|
||||||
<div className="b-inner">
|
|
||||||
<h1 className="b-news-page__title">6 learnings from Shivpuri to Silicon Valley</h1>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="b-news-page__text">
|
|
||||||
{`news id ${params.blogId}`}<br />
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my experiences, learnings,
|
|
||||||
and best practices which helped me to grow both in my personal and professional life. My hope is to
|
|
||||||
give back to the community and help anyone connect directly with me who may have got impacted with
|
|
||||||
recent layoffs, dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
<div className="b-news-page__image">
|
|
||||||
<img className="" src="/images/news1.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="b-news-page__inner">
|
|
||||||
<h2 className="title-h2">
|
|
||||||
This is not about layoffs, it's about living with whatever life throws at you..
|
|
||||||
</h2>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Over the past few months, as the macro-economic events have unfolded, I have heard voices filled
|
|
||||||
with anxiety, helplessness and general lack of confidence to deal with this ambiguity from my
|
|
||||||
mentees, colleagues, friends and family. I was laid off from Meta last November and I firmly
|
|
||||||
believe this is nothing but a bump in the road that might seem like a steep climb in the
|
|
||||||
short-term. I may not have all the answers but this has inspired me to share my story. If you
|
|
||||||
are looking for a sob story, you can stop reading now. Ever wondered what it takes for a girl
|
|
||||||
born into a conservative family in a small sleepy town in India, who lost one of her parents at
|
|
||||||
age 17, earned her living while pursuing engineering, moved to the UK by herself and ended up
|
|
||||||
working in big tech in Silicon valley? My goal with this series of posts is to inspire and share
|
|
||||||
my mental models that helped me throughout my professional and personal life.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
After completing my engineering, I started my career at a small software company in Bhopal and
|
|
||||||
then worked for TCS(Tata Consultancy Services), one of the largest IT-outsourcing companies in
|
|
||||||
the world for almost 5 years. Over the past 14 years, I have worked for big tech companies like
|
|
||||||
Meta (Facebook) and Google, wore multiple hats, led strategic programs, scaled multi
|
|
||||||
billion-dollar businesses, built teams and helped achieve business operational excellence.
|
|
||||||
Throughout my career, I’ve dealt with several challenges from execution to scale to building a
|
|
||||||
high performance team. A lot of my early struggles were about how to assimilate in a new
|
|
||||||
culture, create a network in a new environment, earn trust, create and nurture work
|
|
||||||
relationships into fruitful friendships and so on.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
I was born in a conservative family in a small town called ‘Shivpuri’, also known as ‘Mini
|
|
||||||
Kashmir’ because of its natural beauty. My father was a civil engineer working on Madikheda Dam
|
|
||||||
on Sindh river and was a strict disciplinarian. He was gone from dawn to dusk and was always
|
|
||||||
focused. My mother was a teacher in a school that was about 30 kms from our home. We (me and my
|
|
||||||
sister) would often be left with neighbors to be taken care of and this led us to become
|
|
||||||
independent at an early age. Our otherwise slow paced, simple life with only a few families
|
|
||||||
around in the government quarters that were set up to support construction of the dam was filled
|
|
||||||
with natural beauty, wildlife and a community of close friends. Our lives were balanced and
|
|
||||||
while my parents worked hard to provide basic needs, we were satisfied. There were only a few
|
|
||||||
schools with Hindi being the prevalent language as the medium of teaching. There were no
|
|
||||||
colleges for advanced studies and most girls did not go to college often married off by their
|
|
||||||
18th birthday. Generally speaking, we had a joyous childhood with just the basics. While most
|
|
||||||
folks we interacted with were not highly educated nor ambitious, earned lower middle class
|
|
||||||
salaries and lacked exposure to the outside world but there was plenty to learn from them.
|
|
||||||
People had learnt to stick together in good and bad times. They embodied the old school
|
|
||||||
qualities of hard work, dedication and commitment. Be willing to give it all- hard work,
|
|
||||||
dedication and commitment.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
In 2003, my father passed away suddenly and we found ourselves in crisis. My mother was a
|
|
||||||
teacher and she did not have time to deal with her grief. Rather, she was struggling to garner
|
|
||||||
support to get transferred to a school in Bhopal, capital of Madhya Pradesh to be closer to our
|
|
||||||
maternal grandparents. As we uprooted ourselves from Shivpuri to Bhopal, one of my father’s
|
|
||||||
loyal friends came to help load the moving truck. While he had nothing to gain out of us, he
|
|
||||||
continued to serve us until the last day in Shivpuri. Remember, in crisis your team matters more
|
|
||||||
than any other time. Advocate for them ruthlessly in good and bad times, they will come through
|
|
||||||
in crisis.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Eventually we found our footing, my mother’s job was transferred to a local school in Bhopal and
|
|
||||||
I got admission in a government engineering college. My sister was still attending high school
|
|
||||||
and both of us were teaching tuition classes to middle school students in the evenings to make
|
|
||||||
ends meet. I also started a tiffin service for a few out of town students while attending
|
|
||||||
college to pay for my transportation and cost of supplies. We refused to give up. Persevere when
|
|
||||||
all else fails.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
Our 5 years went by quickly in Bhopal as we worked towards improving our financial situation and
|
|
||||||
I completed my Bachelors in Computer Science. This was the time I first stepped out to live in a
|
|
||||||
metropolitan city, Mumbai for my job at TCS. This was a paradigm shift from Bhopal and I was
|
|
||||||
blown away to meet so many talented folks in Mumbai. In my head, I did not belong in this place.
|
|
||||||
I had imposter syndrome and felt like an outsider trying to make it in a new city. Most people I
|
|
||||||
met were fluent in more than 1 language, well-dressed, communicated openly and with confidence,
|
|
||||||
and presented themselves well. I was always in a dilemma when it came to adopting values. It
|
|
||||||
took me a while to adjust to it but I was still not confident about my work and communication
|
|
||||||
while my hard skills that I learnt in engineering were top notch. I kept questioning my
|
|
||||||
abilities but persisted. This was not the first time I was out of my comfort zone. Persist, when
|
|
||||||
in discomfort.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
I worked with multiple global companies who were clients of TCS and was presented an opportunity
|
|
||||||
to move to Scotland, UK for an year to work for GE, who was also a client. This was my first
|
|
||||||
opportunity to explore a different culture, food, music, languages etc. I remember working on my
|
|
||||||
english when in Mumbai, in preparation for my UK trip. It was really difficult to understand the
|
|
||||||
accent in the UK, even though language was not a barrier. I still remember certain words would
|
|
||||||
just not get across no matter how hard some of my colleagues tried and they would end up using
|
|
||||||
signs to convey. Be prepared, opportunities come to those who are prepared.
|
|
||||||
</p>
|
|
||||||
<p className="b-news-page__text">
|
|
||||||
In 2013, I came to the US on a dependent visa after marriage and quickly realized the curse of
|
|
||||||
H4 visa. I paved my path by going back to school at UC Berkeley and then jumped back into
|
|
||||||
building my career from scratch. While working in the US over the past years, I realized college
|
|
||||||
degrees with good grades and certifications definitely help you to get your foot in the door but
|
|
||||||
are not enough to be successful in your career. As I was again starting from scratch in a new
|
|
||||||
culture, determined to do whatever it takes, having done this a few times before, it doesn’t
|
|
||||||
scare me as much. Never be afraid to start from zero again!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/blogPosts";
|
||||||
|
import Util from "node:util";
|
||||||
|
import RichText from "../../../../lib/contentful/RichText";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Bbuddy - Blog item',
|
||||||
|
description: 'Bbuddy desc blog item'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
|
||||||
|
const blogPosts = await fetchBlogPosts({ preview: false })
|
||||||
|
|
||||||
|
return blogPosts.map((post) => ({ slug: post.slug }))
|
||||||
|
}
|
||||||
|
function renderWidget (widget: Widget) {
|
||||||
|
switch (widget.type){
|
||||||
|
case 'widgetParagraph':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className="title-h2">
|
||||||
|
{widget.widget.subTitle}
|
||||||
|
</h2>
|
||||||
|
<RichText document={widget.widget.body} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 'widgetMedia':
|
||||||
|
return (
|
||||||
|
<img src={widget.widget.file?.src}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogItem({params}: { params: BlogPostPageParams }) {
|
||||||
|
const item = await fetchBlogPost({slug: params.slug, preview: draftMode().isEnabled })
|
||||||
|
console.log('BLOG POST')
|
||||||
|
console.log(Util.inspect(item, {showHidden: false, depth: null, colors: true}))
|
||||||
|
if (!item) notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="b-news-page">
|
||||||
|
<div className="b-inner">
|
||||||
|
<h1 className="b-news-page__title">{item.title}</h1>
|
||||||
|
<div className="news-item__badge">{item.category}</div>
|
||||||
|
<div className="b-news-page__text">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="b-news-page__image">
|
||||||
|
<img className="" src="/images/news1.png" alt="" />
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info">
|
||||||
|
<div className="news-item__info__author">
|
||||||
|
<img className="" src={item.author.avatar.src} alt="" />
|
||||||
|
<div className="news-item__info__author__inner">
|
||||||
|
<div className="news-item__info__name">{item.author?.name}</div>
|
||||||
|
<div className="news-item__info__date">{item.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__counter">
|
||||||
|
<div className="news-item__info__like">
|
||||||
|
<img className="" src="/images/heart-outline.svg" alt="" />
|
||||||
|
165
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__share">
|
||||||
|
<img className="" src="/images/share-social.svg" alt="" />
|
||||||
|
Share
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-news-page__inner">
|
||||||
|
{item.body.map(renderWidget)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
|
||||||
|
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Bbuddy - Blog',
|
||||||
|
description: 'Bbuddy desc blog'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BlogPostPageParams {
|
||||||
|
slug: string
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
interface BlogPostPageProps {
|
||||||
|
params: BlogPostPageParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Blog({params}: { params: BlogPostPageParams }) {
|
||||||
|
unstable_setRequestLocale(params.locale);
|
||||||
|
const data = await fetchBlogPosts({ preview: draftMode().isEnabled, locale: params.locale, category: params.slug })
|
||||||
|
const cats = await fetchBlogPostCategories(false)
|
||||||
|
return (
|
||||||
|
<div className="b-news">
|
||||||
|
<div className="b-news__header">
|
||||||
|
<div className="b-inner">
|
||||||
|
<h1 className="title-h1">
|
||||||
|
Mentorship, Career <br/>
|
||||||
|
Development & Coaching.
|
||||||
|
</h1>
|
||||||
|
<div className="wrap-text">
|
||||||
|
<p className="">The ins-and-outs of building a career in tech, gaining <br/> experience</p>
|
||||||
|
<p className="">from a mentor, and getting your feet wet with coaching.</p>
|
||||||
|
</div>
|
||||||
|
<div className="b-news__header__img">
|
||||||
|
<img className="" src="/images/news-top.png" alt=""/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-news__filter ">
|
||||||
|
<div className="b-inner">
|
||||||
|
<div className="wrap-filter">
|
||||||
|
{
|
||||||
|
cats.map((cat, i)=>(
|
||||||
|
<Link key={'blogCat'+i} href={'/'+params.locale+'/blog/category/'+cat.slug} className={"filter-item"+(cat.slug === params.slug ? ' active' : '')}>{cat.title}</Link>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="b-news__result-list">
|
||||||
|
<div className="b-inner">
|
||||||
|
<div className="news-list">
|
||||||
|
{data.map((item, i) => (
|
||||||
|
<li key={'blogPost'+i} className="list-sidebar__item">
|
||||||
|
<Link href={'/'+params.locale+'/blog/'+item.slug} className="news-item">
|
||||||
|
<div className="news-item__image">
|
||||||
|
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__inner">
|
||||||
|
<div className="">
|
||||||
|
<div className="news-item__title">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="news-item__badge">{item.category}</div>
|
||||||
|
<div className="news-item__text">
|
||||||
|
{item.excerpt}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info">
|
||||||
|
<div className="news-item__info__author">
|
||||||
|
<img className="" src={item.author.avatar.src} alt=""/>
|
||||||
|
<div className="news-item__info__author__inner">
|
||||||
|
<div className="news-item__info__name">{item.author.name}</div>
|
||||||
|
<div className="news-item__info__date">{item.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__counter">
|
||||||
|
<div className="news-item__info__like">
|
||||||
|
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||||
|
165
|
||||||
|
</div>
|
||||||
|
<div className="news-item__info__share">
|
||||||
|
<img className="" src="/images/share-social.svg" alt=""/>
|
||||||
|
Share
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,12 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import * as Util from "node:util";
|
||||||
|
import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
|
||||||
|
import {unstable_setRequestLocale} from "next-intl/server";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Bbuddy - Blog',
|
title: 'Bbuddy - Blog',
|
||||||
description: 'Bbuddy desc blog'
|
description: 'Bbuddy desc blog'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Blog() {
|
|
||||||
|
|
||||||
|
export default async function Blog({ params: { locale } }: { params: { locale: string } }) {
|
||||||
|
unstable_setRequestLocale(locale);
|
||||||
|
const data = await fetchBlogPosts(false, locale)
|
||||||
|
const cats = await fetchBlogPostCategories(false)
|
||||||
return (
|
return (
|
||||||
<div className="b-news">
|
<div className="b-news">
|
||||||
<div className="b-news__header">
|
<div className="b-news__header">
|
||||||
|
@ -27,49 +37,39 @@ export default function Blog() {
|
||||||
<div className="b-news__filter ">
|
<div className="b-news__filter ">
|
||||||
<div className="b-inner">
|
<div className="b-inner">
|
||||||
<div className="wrap-filter">
|
<div className="wrap-filter">
|
||||||
<a href="#" className="filter-item">Leadership & Management</a>
|
{
|
||||||
<a href="#" className="filter-item">Professional Development</a>
|
cats.map((cat, i)=>(
|
||||||
<a href="#" className="filter-item">Research & Insights</a>
|
<Link href={'category/'+cat.slug} className="filter-item">{cat.title}</Link>
|
||||||
<a href="#" className="filter-item">Well-Being</a>
|
))
|
||||||
<a href="#" className="filter-item">Diversity & Inclusion</a>
|
}
|
||||||
<a href="#" className="filter-item">Culture</a>
|
|
||||||
<a href="#" className="filter-item">Sales</a>
|
|
||||||
<a href="#" className="filter-item">Collaboration</a>
|
|
||||||
<a href="#" className="filter-item">Hiring</a>
|
|
||||||
<a href="#" className="filter-item active">BBuddy product</a>
|
|
||||||
<a href="#" className="filter-item">Customer Stories</a>
|
|
||||||
<a href="#" className="filter-item">Coaching</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="b-news__result-list">
|
<div className="b-news__result-list">
|
||||||
<div className="b-inner">
|
<div className="b-inner">
|
||||||
<div className="news-list">
|
<div className="news-list">
|
||||||
<a href="#" className="news-item">
|
{data.map((item, i) => (
|
||||||
|
<li key={'blog'+i} className="list-sidebar__item">
|
||||||
|
<Link href={`${item.slug}`} className="news-item">
|
||||||
<div className="news-item__image">
|
<div className="news-item__image">
|
||||||
<img className="" src="/images/news.png" alt="" />
|
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-item__inner">
|
<div className="news-item__inner">
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="news-item__title">
|
<div className="news-item__title">
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
<div className="news-item__badge">{item.category}</div>
|
||||||
<div className="news-item__text">
|
<div className="news-item__text">
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
{item.excerpt}
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-item__info">
|
<div className="news-item__info">
|
||||||
<div className="news-item__info__author">
|
<div className="news-item__info__author">
|
||||||
<img className="" src="/images/author.png" alt="" />
|
<img className="" src={item.author.avatar.src} alt=""/>
|
||||||
<div className="news-item__info__author__inner">
|
<div className="news-item__info__author__inner">
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
<div className="news-item__info__name">{item.author.name}</div>
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
<div className="news-item__info__date">{item.createdAt}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-item__info__counter">
|
<div className="news-item__info__counter">
|
||||||
|
@ -84,127 +84,9 @@ export default function Blog() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</Link>
|
||||||
<a href="#" className="news-item">
|
</li>
|
||||||
<div className="news-item__image">
|
))}
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" className="news-item">
|
|
||||||
<div className="news-item__image">
|
|
||||||
<img className="" src="/images/news.png" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="news-item__inner">
|
|
||||||
<div className="">
|
|
||||||
<div className="news-item__title">
|
|
||||||
6 learnings from Shivpuri to Silicon Valley
|
|
||||||
</div>
|
|
||||||
<div className="news-item__badge">Leadership & Management</div>
|
|
||||||
<div className="news-item__text">
|
|
||||||
I’m excited to kick off this series of newsletters where I’ll be sharing my
|
|
||||||
experiences,
|
|
||||||
learnings, and best practices which helped me to grow both in my personal and
|
|
||||||
professional life. My hope is to give back to the community and help anyone
|
|
||||||
connect directly with me who may have got impacted with recent layoffs,
|
|
||||||
dealing with immigration challenges.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info">
|
|
||||||
<div className="news-item__info__author">
|
|
||||||
<img className="" src="/images/author.png" alt="" />
|
|
||||||
<div className="news-item__info__author__inner">
|
|
||||||
<div className="news-item__info__name">Sonali Garg</div>
|
|
||||||
<div className="news-item__info__date">February 6th, 2023</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__counter">
|
|
||||||
<div className="news-item__info__like">
|
|
||||||
<img className="" src="/images/heart-outline.svg" alt="" />
|
|
||||||
165
|
|
||||||
</div>
|
|
||||||
<div className="news-item__info__share">
|
|
||||||
<img className="" src="/images/share-social.svg" alt="" />
|
|
||||||
Share
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const Agora = ({ sessionId, secret, stopCalling, remoteUser }: AgoraProps
|
||||||
|
|
||||||
useJoin(
|
useJoin(
|
||||||
{
|
{
|
||||||
appid: 'ed90c9dc42634e5687d4e2e0766b363f',
|
appid: process.env.NEXT_PUBLIC_AGORA_APPID,
|
||||||
channel: `${sessionId}-${secret}`,
|
channel: `${sessionId}-${secret}`,
|
||||||
token: null,
|
token: null,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { ICameraVideoTrack, LocalVideoTrack, LocalVideoTrackProps, MaybePromiseOrNull } from 'agora-rtc-react';
|
||||||
|
import { useAwaited } from '../../../../utils/agora/tools';
|
||||||
|
|
||||||
|
interface CameraVideoTrackProps extends LocalVideoTrackProps {
|
||||||
|
/**
|
||||||
|
* A camera video track which can be created by `createCameraVideoTrack()`.
|
||||||
|
*/
|
||||||
|
readonly track?: MaybePromiseOrNull<ICameraVideoTrack>;
|
||||||
|
/**
|
||||||
|
* Device ID, which can be retrieved by calling `getDevices()`.
|
||||||
|
*/
|
||||||
|
readonly deviceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CameraVideoTrack = ({
|
||||||
|
track: maybeTrack,
|
||||||
|
deviceId,
|
||||||
|
...props
|
||||||
|
}: CameraVideoTrackProps) => {
|
||||||
|
const track = useAwaited(maybeTrack);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (track && deviceId != null) {
|
||||||
|
track.setDevice(deviceId).catch(console.warn);
|
||||||
|
}
|
||||||
|
}, [deviceId, track]);
|
||||||
|
|
||||||
|
return <LocalVideoTrack track={maybeTrack} {...props} />;
|
||||||
|
};
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { HTMLProps, ReactNode } from 'react';
|
||||||
|
import { ICameraVideoTrack, IMicrophoneAudioTrack, MaybePromiseOrNull } from 'agora-rtc-react';
|
||||||
|
import { UserCover } from '../components';
|
||||||
|
import { MicrophoneAudioTrack } from './MicrophoneAudioTrack';
|
||||||
|
import { CameraVideoTrack } from './CameraVideoTrack';
|
||||||
|
|
||||||
|
interface LocalUserProps extends HTMLProps<HTMLDivElement> {
|
||||||
|
/**
|
||||||
|
* Whether to turn on the local user's microphone. Default false.
|
||||||
|
*/
|
||||||
|
readonly micOn?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to turn on the local user's camera. Default false.
|
||||||
|
*/
|
||||||
|
readonly cameraOn?: boolean;
|
||||||
|
/**
|
||||||
|
* A microphone audio track which can be created by `createMicrophoneAudioTrack()`.
|
||||||
|
*/
|
||||||
|
readonly audioTrack?: MaybePromiseOrNull<IMicrophoneAudioTrack>;
|
||||||
|
/**
|
||||||
|
* A camera video track which can be created by `createCameraVideoTrack()`.
|
||||||
|
*/
|
||||||
|
readonly videoTrack?: MaybePromiseOrNull<ICameraVideoTrack>;
|
||||||
|
/**
|
||||||
|
* Whether to play the local user's audio track. Default follows `micOn`.
|
||||||
|
*/
|
||||||
|
readonly playAudio?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to play the local user's video track. Default follows `cameraOn`.
|
||||||
|
*/
|
||||||
|
readonly playVideo?: boolean;
|
||||||
|
/**
|
||||||
|
* Device ID, which can be retrieved by calling `getDevices()`.
|
||||||
|
*/
|
||||||
|
readonly micDeviceId?: string;
|
||||||
|
/**
|
||||||
|
* Device ID, which can be retrieved by calling `getDevices()`.
|
||||||
|
*/
|
||||||
|
readonly cameraDeviceId?: string;
|
||||||
|
/**
|
||||||
|
* The volume. The value ranges from 0 (mute) to 1000 (maximum). A value of 100 is the current volume.
|
||||||
|
*/
|
||||||
|
readonly volume?: number;
|
||||||
|
/**
|
||||||
|
* Render cover image if playVideo is off.
|
||||||
|
*/
|
||||||
|
readonly cover?: string;
|
||||||
|
/**
|
||||||
|
* Children is rendered on top of the video canvas.
|
||||||
|
*/
|
||||||
|
readonly children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play/Stop local user camera and microphone track.
|
||||||
|
*/
|
||||||
|
export function LocalUser({
|
||||||
|
micOn,
|
||||||
|
cameraOn,
|
||||||
|
audioTrack,
|
||||||
|
videoTrack,
|
||||||
|
playAudio = false,
|
||||||
|
playVideo,
|
||||||
|
micDeviceId,
|
||||||
|
cameraDeviceId,
|
||||||
|
volume,
|
||||||
|
cover,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: LocalUserProps) {
|
||||||
|
playVideo = playVideo ?? !!cameraOn;
|
||||||
|
playAudio = playAudio ?? !!micOn;
|
||||||
|
return (
|
||||||
|
<div {...props} style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
|
background: "#000",
|
||||||
|
...style
|
||||||
|
}}>
|
||||||
|
<CameraVideoTrack
|
||||||
|
deviceId={cameraDeviceId}
|
||||||
|
disabled={!cameraOn}
|
||||||
|
play={playVideo}
|
||||||
|
track={videoTrack}
|
||||||
|
/>
|
||||||
|
<MicrophoneAudioTrack
|
||||||
|
deviceId={micDeviceId}
|
||||||
|
disabled={!micOn}
|
||||||
|
play={playAudio}
|
||||||
|
track={audioTrack}
|
||||||
|
volume={volume}
|
||||||
|
/>
|
||||||
|
{cover && !cameraOn && <UserCover cover={cover} />}
|
||||||
|
<div style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
|
zIndex: 2,
|
||||||
|
}}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,14 @@
|
||||||
import { LocalUser, useLocalMicrophoneTrack, useLocalCameraTrack, usePublish, useIsConnected } from 'agora-rtc-react';
|
import { useLocalMicrophoneTrack, useLocalCameraTrack, usePublish, useIsConnected } from 'agora-rtc-react';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { UserOutlined } from '@ant-design/icons';
|
import { UserOutlined } from '@ant-design/icons';
|
||||||
import { useLocalStorage } from '../../../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../../../hooks/useLocalStorage';
|
||||||
import { AUTH_USER } from '../../../../constants/common';
|
import { AUTH_USER } from '../../../../constants/common';
|
||||||
|
import { LocalUser } from './LocalUser';
|
||||||
|
|
||||||
type LocalUserPanelProps = {
|
type LocalUserPanelProps = {
|
||||||
calling: boolean;
|
calling: boolean;
|
||||||
micOn: boolean;
|
micOn: boolean;
|
||||||
cameraOn: boolean;
|
cameraOn: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LocalUserPanel = ({
|
export const LocalUserPanel = ({
|
||||||
calling,
|
calling,
|
||||||
|
@ -18,26 +18,11 @@ export const LocalUserPanel = ({
|
||||||
const isConnected = useIsConnected();
|
const isConnected = useIsConnected();
|
||||||
const [userData] = useLocalStorage(AUTH_USER, '');
|
const [userData] = useLocalStorage(AUTH_USER, '');
|
||||||
const { faceImageUrl: userImage = '' } = userData ? JSON.parse(userData) : {};
|
const { faceImageUrl: userImage = '' } = userData ? JSON.parse(userData) : {};
|
||||||
|
|
||||||
const [playVideo, setPlayVideo] = useState(false);
|
|
||||||
const [playAudio, setPlayAudio] = useState(false);
|
|
||||||
|
|
||||||
const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn);
|
const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn);
|
||||||
const { localCameraTrack } = useLocalCameraTrack(cameraOn);
|
const { localCameraTrack } = useLocalCameraTrack(cameraOn);
|
||||||
|
|
||||||
usePublish([localMicrophoneTrack, localCameraTrack]);
|
usePublish([localMicrophoneTrack, localCameraTrack]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (calling) {
|
|
||||||
setPlayVideo(cameraOn)
|
|
||||||
}
|
|
||||||
}, [cameraOn]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (calling) {
|
|
||||||
setPlayAudio(micOn)
|
|
||||||
}
|
|
||||||
}, [micOn]);
|
|
||||||
|
|
||||||
return calling && isConnected ? (
|
return calling && isConnected ? (
|
||||||
<div className="b-agora__local_user">
|
<div className="b-agora__local_user">
|
||||||
{!cameraOn && (
|
{!cameraOn && (
|
||||||
|
@ -51,9 +36,6 @@ export const LocalUserPanel = ({
|
||||||
audioTrack={localMicrophoneTrack}
|
audioTrack={localMicrophoneTrack}
|
||||||
cameraOn={cameraOn}
|
cameraOn={cameraOn}
|
||||||
micOn={micOn}
|
micOn={micOn}
|
||||||
playAudio={playAudio}
|
|
||||||
playVideo={playVideo}
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
videoTrack={localCameraTrack}
|
videoTrack={localCameraTrack}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { ReactNode, useEffect } from 'react';
|
||||||
|
import { IMicrophoneAudioTrack, LocalAudioTrack, LocalAudioTrackProps, MaybePromiseOrNull } from 'agora-rtc-react';
|
||||||
|
import { useAwaited } from '../../../../utils/agora/tools';
|
||||||
|
|
||||||
|
interface MicrophoneAudioTrackProps extends LocalAudioTrackProps {
|
||||||
|
/**
|
||||||
|
* A microphone audio track which can be created by `createMicrophoneAudioTrack()`.
|
||||||
|
*/
|
||||||
|
readonly track?: MaybePromiseOrNull<IMicrophoneAudioTrack>;
|
||||||
|
/**
|
||||||
|
* Device ID, which can be retrieved by calling `getDevices()`.
|
||||||
|
*/
|
||||||
|
readonly deviceId?: string;
|
||||||
|
|
||||||
|
readonly children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MicrophoneAudioTrack = ({
|
||||||
|
track: maybeTrack,
|
||||||
|
deviceId,
|
||||||
|
...props
|
||||||
|
}: MicrophoneAudioTrackProps) => {
|
||||||
|
const track = useAwaited(maybeTrack);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (track && deviceId != null) {
|
||||||
|
track.setDevice(deviceId).catch(console.warn);
|
||||||
|
}
|
||||||
|
}, [deviceId, track]);
|
||||||
|
|
||||||
|
return <LocalAudioTrack track={maybeTrack} {...props} />;
|
||||||
|
};
|
|
@ -40,10 +40,9 @@ export function RemoteVideoPlayer({
|
||||||
...props
|
...props
|
||||||
}: RemoteVideoPlayerProps) {
|
}: RemoteVideoPlayerProps) {
|
||||||
const resolvedClient = useRTCClient(client);
|
const resolvedClient = useRTCClient(client);
|
||||||
const hasVideo = resolvedClient.remoteUsers?.find(
|
const hasVideo = resolvedClient.remoteUsers?.find(user => user.uid === track?.getUserId())?.hasVideo;
|
||||||
user => user.uid === track?.getUserId(),
|
|
||||||
)?.hasVideo;
|
|
||||||
playVideo = playVideo ?? hasVideo;
|
playVideo = playVideo ?? hasVideo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props} style={{
|
<div {...props} style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
@ -66,4 +65,4 @@ export function RemoteVideoPlayer({
|
||||||
}}>{children}</div>
|
}}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -4,8 +4,6 @@ import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react';
|
||||||
import { Session } from '../../../types/sessions';
|
import { Session } from '../../../types/sessions';
|
||||||
import { Agora } from './Agora';
|
import { Agora } from './Agora';
|
||||||
|
|
||||||
AgoraRTC.setLogLevel(0);
|
|
||||||
|
|
||||||
export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => {
|
export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => {
|
||||||
const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach;
|
const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Document as RichTextDocument } from '@contentful/rich-text-types'
|
||||||
|
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
|
||||||
|
|
||||||
|
type RichTextProps = {
|
||||||
|
document: RichTextDocument | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function RichText({ document }: RichTextProps) {
|
||||||
|
if (!document) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{documentToReactComponents(document)}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichText
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { parseContentfulContentImage } from './contentImage'
|
||||||
|
import {Author, AuthorEntry} from "../../types/author";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function parseContentfulAuthor(authorEntry?: AuthorEntry<any, any>): Author | null {
|
||||||
|
if (!authorEntry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: authorEntry.fields.name || '',
|
||||||
|
avatar: parseContentfulContentImage(authorEntry.fields.avatar),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { Entry } from 'contentful'
|
||||||
|
import contentfulClient from './contentfulClient'
|
||||||
|
import { parseContentfulContentImage } from './contentImage'
|
||||||
|
import {BlogPost, BlogPostEntry, BlogPostSkeleton} from "../../types/blogPost";
|
||||||
|
import {parseContentfulAuthor} from "./authors";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {WidgetMedia, WidgetMediaEntry} from "../../types/blogWidgets/widgetMedia";
|
||||||
|
import {WidgetParagraph} from "../../types/blogWidgets/widgetParagraph";
|
||||||
|
import entry from "next/dist/server/typescript/rules/entry";
|
||||||
|
import Util from "node:util";
|
||||||
|
|
||||||
|
type PostEntry = BlogPostEntry<undefined, string>//Entry<BlogPostSkeleton, undefined, string>
|
||||||
|
type widgetEnum = WidgetParagraph | WidgetMedia
|
||||||
|
export type Widget = {
|
||||||
|
widget: widgetEnum
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
type WidgetEntry = WidgetMediaEntry<undefined, string> | WidgetParagraph<undefined, string>
|
||||||
|
function parseWidgets(entries?: Array<WidgetEntry>): Array<Widget> | null {
|
||||||
|
if (!entries || !entries.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return entries.map((entry: WidgetEntry) => {
|
||||||
|
const wType = entry.sys.contentType.sys.id
|
||||||
|
let wObj = {} as widgetEnum
|
||||||
|
switch (wType){
|
||||||
|
case 'widgetParagraph':
|
||||||
|
wObj = {
|
||||||
|
subTitle: entry.fields.subTitle,
|
||||||
|
body: entry.fields.body
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'widgetMedia':
|
||||||
|
wObj = {
|
||||||
|
decription: entry.fields.decription || '',
|
||||||
|
file: parseContentfulContentImage(entry.fields.file)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: wType,
|
||||||
|
widget: wObj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContentfulBlogPost(entry?: PostEntry): BlogPost | null {
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: entry.fields.title || '',
|
||||||
|
slug: entry.fields.slug,
|
||||||
|
excerpt: entry.fields.excerpt || '',
|
||||||
|
listImage: parseContentfulContentImage(entry.fields.listImage),
|
||||||
|
author: parseContentfulAuthor(entry.fields.author),
|
||||||
|
createdAt: dayjs(entry.sys.createdAt).format('MMM DD, YYYY'),
|
||||||
|
category: entry.fields.category.fields.title,
|
||||||
|
body: parseWidgets(entry.fields.body) || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchBlogPostsOptions {
|
||||||
|
preview: boolean
|
||||||
|
local?: string
|
||||||
|
category?: string
|
||||||
|
}
|
||||||
|
export async function fetchBlogPosts({ preview, category }: FetchBlogPostsOptions): Promise<BlogPost[]> {
|
||||||
|
const contentful = contentfulClient({ preview })
|
||||||
|
const query = {
|
||||||
|
content_type: 'blogPost',
|
||||||
|
select: ['fields.title', 'fields.excerpt', 'fields.author', 'fields.listImage', 'fields.author', 'fields.category', 'sys.createdAt', 'fields.slug'],
|
||||||
|
order: ['sys.createdAt'],
|
||||||
|
}
|
||||||
|
if (category){
|
||||||
|
query['fields.category.fields.slug'] = category
|
||||||
|
query['fields.category.sys.contentType.sys.id']='blogPostCategory'
|
||||||
|
}
|
||||||
|
const blogPostsResult = await contentful.getEntries<BlogPostSkeleton>(query)
|
||||||
|
|
||||||
|
return blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchBlogPostOptions {
|
||||||
|
slug: string
|
||||||
|
preview: boolean
|
||||||
|
}
|
||||||
|
export async function fetchBlogPost({ slug, preview }: FetchBlogPostOptions): Promise<BlogPost | null> {
|
||||||
|
const contentful = contentfulClient({ preview })
|
||||||
|
|
||||||
|
const blogPostsResult = await contentful.getEntries<BlogPostSkeleton>({
|
||||||
|
content_type: 'blogPost',
|
||||||
|
'fields.slug': slug,
|
||||||
|
include: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
return parseContentfulBlogPost(blogPostsResult.items[0])
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Entry } from 'contentful'
|
||||||
|
import contentfulClient from './contentfulClient'
|
||||||
|
import { parseContentfulContentImage } from './contentImage'
|
||||||
|
import {BlogPost, BlogPostEntry, BlogPostSkeleton} from "../../types/blogPost";
|
||||||
|
import {parseContentfulAuthor} from "./authors";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import {BlogPostCategory, BlogPostCategoryEntry, BlogPostCategorySkeleton} from "../../types/blogPostCategory";
|
||||||
|
|
||||||
|
type ListEntry = BlogPostCategoryEntry<undefined, string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function parseContentfulBlogPostCategory(entry?: ListEntry): BlogPostCategory | null {
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: entry.fields.title || '',
|
||||||
|
slug: entry.fields.slug || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchListOptions {
|
||||||
|
preview: boolean
|
||||||
|
local?: string
|
||||||
|
}
|
||||||
|
export async function fetchBlogPostCategories({ preview }: FetchListOptions): Promise<BlogPostCategory[]> {
|
||||||
|
const contentful = contentfulClient({ preview })
|
||||||
|
|
||||||
|
const results = await contentful.getEntries<BlogPostCategorySkeleton>({
|
||||||
|
content_type: 'blogPostCategory',
|
||||||
|
order: ['fields.title'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return results.items.map((entry) => parseContentfulBlogPostCategory(entry) as BlogPostCategory)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Asset, AssetLink } from 'contentful'
|
||||||
|
|
||||||
|
export interface ContentImage {
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContentfulContentImage(
|
||||||
|
asset?: Asset<undefined, string> | { sys: AssetLink }
|
||||||
|
): ContentImage | null {
|
||||||
|
if (!asset) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('fields' in asset)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
src: asset.fields.file?.url || '',
|
||||||
|
alt: asset.fields.description || '',
|
||||||
|
width: asset.fields.file?.details.image?.width || 0,
|
||||||
|
height: asset.fields.file?.details.image?.height || 0,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { createClient } from 'contentful'
|
||||||
|
|
||||||
|
const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_PREVIEW_ACCESS_TOKEN } = process.env
|
||||||
|
|
||||||
|
// This is the standard Contentful client. It fetches
|
||||||
|
// content that has been published.
|
||||||
|
const client = createClient({
|
||||||
|
space: CONTENTFUL_SPACE_ID!,
|
||||||
|
accessToken: CONTENTFUL_ACCESS_TOKEN!,
|
||||||
|
})
|
||||||
|
|
||||||
|
// This is a Contentful client that's been configured
|
||||||
|
// to fetch drafts and unpublished content.
|
||||||
|
const previewClient = createClient({
|
||||||
|
space: CONTENTFUL_SPACE_ID!,
|
||||||
|
accessToken: CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
|
||||||
|
host: 'preview.contentful.com',
|
||||||
|
})
|
||||||
|
|
||||||
|
// This little helper will let us switch between the two
|
||||||
|
// clients easily:
|
||||||
|
export default function contentfulClient({ preview = false }) {
|
||||||
|
if (preview) {
|
||||||
|
return previewClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
|
||||||
|
export interface AuthorFields {
|
||||||
|
name: EntryFieldTypes.Symbol
|
||||||
|
avatar: EntryFieldTypes.AssetLink
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Author {
|
||||||
|
name: string
|
||||||
|
avatar: ContentImage | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthorSkeleton = EntrySkeletonType<AuthorFields, 'author'>
|
||||||
|
export type AuthorEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
AuthorSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {Author, AuthorSkeleton} from "./author";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
import {BlogPostCategorySkeleton} from "./blogPostCategory";
|
||||||
|
import {WidgetMedia, WidgetMediaSkeleton} from "./blogWidgets/widgetMedia";
|
||||||
|
import {WidgetParagraph, WidgetParagraphSkeleton} from "./blogWidgets/widgetParagraph";
|
||||||
|
|
||||||
|
export interface BlogPostFields {
|
||||||
|
title?: EntryFieldTypes.Symbol
|
||||||
|
slug: EntryFieldTypes.Symbol
|
||||||
|
excerpt: EntryFieldTypes.Symbol
|
||||||
|
listImage?: EntryFieldTypes.AssetLink
|
||||||
|
author?: AuthorSkeleton
|
||||||
|
category: BlogPostCategorySkeleton
|
||||||
|
createdAt?: EntryFieldTypes.Date
|
||||||
|
body?: Array<WidgetMediaSkeleton | WidgetParagraphSkeleton>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPostFields extends BlogPostFields{
|
||||||
|
body: Array<WidgetMediaSkeleton | WidgetParagraphSkeleton>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
excerpt: string
|
||||||
|
listImage: ContentImage | null
|
||||||
|
author: Author | null
|
||||||
|
category: string
|
||||||
|
createdAt: string
|
||||||
|
body: Array<WidgetMedia | WidgetParagraph>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlogPostSkeleton = EntrySkeletonType<BlogPostFields, 'blogPost'>
|
||||||
|
export type BlogPostEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
BlogPostSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
|
||||||
|
export interface BlogPostCategoryFields {
|
||||||
|
title: EntryFieldTypes.Symbol
|
||||||
|
slug: EntryFieldTypes.Symbol
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPostCategory {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlogPostCategorySkeleton = EntrySkeletonType<BlogPostCategoryFields, 'blogPostCategory'>
|
||||||
|
export type BlogPostCategoryEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
BlogPostCategorySkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
|
||||||
|
export interface WidgetMediaFields {
|
||||||
|
decription?: EntryFieldTypes.Symbol
|
||||||
|
file?: EntryFieldTypes.AssetLink
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetMedia {
|
||||||
|
file: ContentImage | null
|
||||||
|
decription: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WidgetMediaSkeleton = EntrySkeletonType<WidgetMediaFields, 'WidgetMedia'>
|
||||||
|
export type WidgetMediaEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
WidgetMediaSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful'
|
||||||
|
import {BlogPostFields} from "./blogPost";
|
||||||
|
import {ContentImage} from "../lib/contentful/contentImage";
|
||||||
|
import { Document as RichTextDocument } from '@contentful/rich-text-types'
|
||||||
|
export interface WidgetParagraphFields {
|
||||||
|
subTitle?: EntryFieldTypes.Symbol
|
||||||
|
body?: EntryFieldTypes.RichText
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetParagraph {
|
||||||
|
subTitle: string
|
||||||
|
body: RichTextDocument | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WidgetParagraphSkeleton = EntrySkeletonType<WidgetParagraphFields, 'WidgetParagraph'>
|
||||||
|
export type WidgetParagraphEntry<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
|
||||||
|
WidgetParagraphSkeleton,
|
||||||
|
Modifiers,
|
||||||
|
Locales
|
||||||
|
>
|
Loading…
Reference in New Issue