blog #1

Merged
Dasha merged 5 commits from blog into develop 2024-08-16 23:49:23 +00:00
24 changed files with 801 additions and 383 deletions

6
.env Normal file
View File

@ -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

View File

@ -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",

View File

@ -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 />
Im excited to kick off this series of newsletters where Ill 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, Ive 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 fathers
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 mothers 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 doesnt
scare me as much. Never be afraid to start from zero again!
</p>
</div>
</div>
</div>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -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">
Im excited to kick off this series of newsletters where Ill 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">
Im excited to kick off this series of newsletters where Ill 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">
Im excited to kick off this series of newsletters where Ill 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">
Im excited to kick off this series of newsletters where Ill 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>

View File

@ -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,
}, },

View File

@ -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} />;
};

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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} />;
};

View File

@ -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>
); );
} };

View File

@ -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;

View File

@ -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

View File

@ -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),
}
}

View File

@ -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])
}

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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
}

20
src/types/author.ts Normal file
View File

@ -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
>

39
src/types/blogPost.ts Normal file
View File

@ -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
>

View File

@ -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
>

View File

@ -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
>

View File

@ -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
>