diff --git a/.env b/.env new file mode 100644 index 0000000..509d982 --- /dev/null +++ b/.env @@ -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 \ No newline at end of file diff --git a/package.json b/package.json index fdfb4c7..4b6b281 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "@ant-design/cssinjs": "^1.18.1", "@ant-design/icons": "^5.2.6", "@ant-design/nextjs-registry": "^1.0.0", + "@contentful/rich-text-react-renderer": "^15.22.9", "agora-rtc-react": "^2.1.0", "agora-rtc-sdk-ng": "^4.20.2", "antd": "^5.12.1", "antd-img-crop": "^4.21.0", "axios": "^1.6.5", + "contentful": "^10.13.3", "dayjs": "^1.11.10", "lodash": "^4.17.21", "next": "14.0.3", diff --git a/src/app/[locale]/blog/[blogId]/page.tsx b/src/app/[locale]/blog/[blogId]/page.tsx deleted file mode 100644 index 131333a..0000000 --- a/src/app/[locale]/blog/[blogId]/page.tsx +++ /dev/null @@ -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 ( -
-
-

6 learnings from Shivpuri to Silicon Valley

-
Leadership & Management
-
- {`news id ${params.blogId}`}
- 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. -
-
- -
-
-
- -
-
Sonali Garg
-
February 6th, 2023
-
-
-
-
- - 165 -
-
- - Share -
-
-
-
-

- This is not about layoffs, it's about living with whatever life throws at you.. -

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-
-
-
- ); -}; diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx new file mode 100644 index 0000000..d1573dc --- /dev/null +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -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 { + const blogPosts = await fetchBlogPosts({ preview: false }) + + return blogPosts.map((post) => ({ slug: post.slug })) +} +function renderWidget (widget: Widget) { + switch (widget.type){ + case 'widgetParagraph': + return ( + <> +

+ {widget.widget.subTitle} +

+ + + ) + case 'widgetMedia': + return ( + + ) + } +} + +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 ( +
+
+

{item.title}

+
{item.category}
+
+ +
+
+ +
+
+
+ +
+
{item.author?.name}
+
{item.createdAt}
+
+
+
+
+ + 165 +
+
+ + Share +
+
+
+
+ {item.body.map(renderWidget)} +
+
+
+ ); +}; diff --git a/src/app/[locale]/blog/category/[slug]/page.tsx b/src/app/[locale]/blog/category/[slug]/page.tsx new file mode 100644 index 0000000..696247c --- /dev/null +++ b/src/app/[locale]/blog/category/[slug]/page.tsx @@ -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 ( +
+
+
+

+ Mentorship, Career
+ Development & Coaching. +

+
+

The ins-and-outs of building a career in tech, gaining
experience

+

from a mentor, and getting your feet wet with coaching.

+
+
+ +
+
+
+
+
+
+ { + cats.map((cat, i)=>( + {cat.title} + )) + } +
+
+
+
+
+
+ {data.map((item, i) => ( +
  • + +
    + {item.listImage?.alt}/ +
    +
    +
    +
    + {item.title} +
    +
    {item.category}
    +
    + {item.excerpt} +
    +
    +
    +
    + +
    +
    {item.author.name}
    +
    {item.createdAt}
    +
    +
    +
    +
    + + 165 +
    +
    + + Share +
    +
    +
    +
    + +
  • + ))} +
    +
    +
    +
    + ); +} diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 20f2989..3ce264d 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -1,210 +1,92 @@ import React from 'react'; 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 = { title: 'Bbuddy - 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 (

    - Mentorship, Career
    + Mentorship, Career
    Development & Coaching.

    -

    The ins-and-outs of building a career in tech, gaining
    experience

    +

    The ins-and-outs of building a career in tech, gaining
    experience

    from a mentor, and getting your feet wet with coaching.

    - +
    - -
    - -
    -
    -
    -
    - 6 learnings from Shivpuri to Silicon Valley + {data.map((item, i) => ( +
  • + +
    + {item.listImage?.alt}/
    -
    Leadership & Management
    -
    - 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. -
    -
  • -
    -
    - -
    -
    Sonali Garg
    -
    February 6th, 2023
    +
    +
    +
    + {item.title} +
    +
    {item.category}
    +
    + {item.excerpt} +
    +
    +
    +
    + +
    +
    {item.author.name}
    +
    {item.createdAt}
    +
    +
    +
    +
    + + 165 +
    +
    + + Share +
    +
    -
    -
    - - 165 -
    -
    - - Share -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - 6 learnings from Shivpuri to Silicon Valley -
    -
    Leadership & Management
    -
    - 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. -
    -
    -
    -
    - -
    -
    Sonali Garg
    -
    February 6th, 2023
    -
    -
    -
    -
    - - 165 -
    -
    - - Share -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - 6 learnings from Shivpuri to Silicon Valley -
    -
    Leadership & Management
    -
    - 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. -
    -
    -
    -
    - -
    -
    Sonali Garg
    -
    February 6th, 2023
    -
    -
    -
    -
    - - 165 -
    -
    - - Share -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - 6 learnings from Shivpuri to Silicon Valley -
    -
    Leadership & Management
    -
    - 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. -
    -
    -
    -
    - -
    -
    Sonali Garg
    -
    February 6th, 2023
    -
    -
    -
    -
    - - 165 -
    -
    - - Share -
    -
    -
    -
    -
    + + + ))}
    diff --git a/src/components/Account/agora/Agora.tsx b/src/components/Account/agora/Agora.tsx index f63be12..6be491d 100644 --- a/src/components/Account/agora/Agora.tsx +++ b/src/components/Account/agora/Agora.tsx @@ -24,7 +24,7 @@ export const Agora = ({ sessionId, secret, stopCalling, remoteUser }: AgoraProps useJoin( { - appid: 'ed90c9dc42634e5687d4e2e0766b363f', + appid: process.env.NEXT_PUBLIC_AGORA_APPID, channel: `${sessionId}-${secret}`, token: null, }, diff --git a/src/components/Account/agora/components/CameraVideoTrack.tsx b/src/components/Account/agora/components/CameraVideoTrack.tsx new file mode 100644 index 0000000..668f51b --- /dev/null +++ b/src/components/Account/agora/components/CameraVideoTrack.tsx @@ -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; + /** + * 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 ; +}; diff --git a/src/components/Account/agora/components/LocalUser.tsx b/src/components/Account/agora/components/LocalUser.tsx new file mode 100644 index 0000000..5d5bf01 --- /dev/null +++ b/src/components/Account/agora/components/LocalUser.tsx @@ -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 { + /** + * 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; + /** + * A camera video track which can be created by `createCameraVideoTrack()`. + */ + readonly videoTrack?: MaybePromiseOrNull; + /** + * 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 ( +
    + + + {cover && !cameraOn && } +
    {children}
    +
    + ); +}; diff --git a/src/components/Account/agora/components/LocalUserPanel.tsx b/src/components/Account/agora/components/LocalUserPanel.tsx index d76c908..87587bd 100644 --- a/src/components/Account/agora/components/LocalUserPanel.tsx +++ b/src/components/Account/agora/components/LocalUserPanel.tsx @@ -1,14 +1,14 @@ -import { LocalUser, useLocalMicrophoneTrack, useLocalCameraTrack, usePublish, useIsConnected } from 'agora-rtc-react'; -import { useState, useEffect } from 'react'; +import { useLocalMicrophoneTrack, useLocalCameraTrack, usePublish, useIsConnected } from 'agora-rtc-react'; import { UserOutlined } from '@ant-design/icons'; import { useLocalStorage } from '../../../../hooks/useLocalStorage'; import { AUTH_USER } from '../../../../constants/common'; +import { LocalUser } from './LocalUser'; type LocalUserPanelProps = { calling: boolean; micOn: boolean; cameraOn: boolean; -} +}; export const LocalUserPanel = ({ calling, @@ -18,26 +18,11 @@ export const LocalUserPanel = ({ const isConnected = useIsConnected(); const [userData] = useLocalStorage(AUTH_USER, ''); const { faceImageUrl: userImage = '' } = userData ? JSON.parse(userData) : {}; - - const [playVideo, setPlayVideo] = useState(false); - const [playAudio, setPlayAudio] = useState(false); - const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn); const { localCameraTrack } = useLocalCameraTrack(cameraOn); + usePublish([localMicrophoneTrack, localCameraTrack]); - useEffect(() => { - if (calling) { - setPlayVideo(cameraOn) - } - }, [cameraOn]); - - useEffect(() => { - if (calling) { - setPlayAudio(micOn) - } - }, [micOn]); - return calling && isConnected ? (
    {!cameraOn && ( @@ -51,9 +36,6 @@ export const LocalUserPanel = ({ audioTrack={localMicrophoneTrack} cameraOn={cameraOn} micOn={micOn} - playAudio={playAudio} - playVideo={playVideo} - style={{ width: '100%', height: '100%' }} videoTrack={localCameraTrack} />
    diff --git a/src/components/Account/agora/components/MicrophoneAudioTrack.tsx b/src/components/Account/agora/components/MicrophoneAudioTrack.tsx new file mode 100644 index 0000000..3876449 --- /dev/null +++ b/src/components/Account/agora/components/MicrophoneAudioTrack.tsx @@ -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; + /** + * 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 ; +}; diff --git a/src/components/Account/agora/components/RemoteVideoPlayer.tsx b/src/components/Account/agora/components/RemoteVideoPlayer.tsx index b5014f5..bb61164 100644 --- a/src/components/Account/agora/components/RemoteVideoPlayer.tsx +++ b/src/components/Account/agora/components/RemoteVideoPlayer.tsx @@ -31,39 +31,38 @@ export interface RemoteVideoPlayerProps extends HTMLProps { * An `IRemoteVideoTrack` can only be own by one `RemoteVideoPlayer`. */ export function RemoteVideoPlayer({ - track, - playVideo, - cover, - client, - style, - children, - ...props + track, + playVideo, + cover, + client, + style, + children, + ...props }: RemoteVideoPlayerProps) { - const resolvedClient = useRTCClient(client); - const hasVideo = resolvedClient.remoteUsers?.find( - user => user.uid === track?.getUserId(), - )?.hasVideo; - playVideo = playVideo ?? hasVideo; - return ( -
    - - {cover && !playVideo && } -
    {children}
    -
    - ); -} + const resolvedClient = useRTCClient(client); + const hasVideo = resolvedClient.remoteUsers?.find(user => user.uid === track?.getUserId())?.hasVideo; + playVideo = playVideo ?? hasVideo; + + return ( +
    + + {cover && !playVideo && } +
    {children}
    +
    + ); +}; diff --git a/src/components/Account/agora/index.tsx b/src/components/Account/agora/index.tsx index d4d23f9..cbca252 100644 --- a/src/components/Account/agora/index.tsx +++ b/src/components/Account/agora/index.tsx @@ -4,8 +4,6 @@ import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react'; import { Session } from '../../../types/sessions'; import { Agora } from './Agora'; -AgoraRTC.setLogLevel(0); - export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => { const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach; diff --git a/src/lib/contentful/RichText.tsx b/src/lib/contentful/RichText.tsx new file mode 100644 index 0000000..597d70b --- /dev/null +++ b/src/lib/contentful/RichText.tsx @@ -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 \ No newline at end of file diff --git a/src/lib/contentful/authors.ts b/src/lib/contentful/authors.ts new file mode 100644 index 0000000..57a2f30 --- /dev/null +++ b/src/lib/contentful/authors.ts @@ -0,0 +1,16 @@ +import { parseContentfulContentImage } from './contentImage' +import {Author, AuthorEntry} from "../../types/author"; + + + + +export function parseContentfulAuthor(authorEntry?: AuthorEntry): Author | null { + if (!authorEntry) { + return null + } + + return { + name: authorEntry.fields.name || '', + avatar: parseContentfulContentImage(authorEntry.fields.avatar), + } +} \ No newline at end of file diff --git a/src/lib/contentful/blogPosts.ts b/src/lib/contentful/blogPosts.ts new file mode 100644 index 0000000..6fa939b --- /dev/null +++ b/src/lib/contentful/blogPosts.ts @@ -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//Entry +type widgetEnum = WidgetParagraph | WidgetMedia +export type Widget = { + widget: widgetEnum + type: string +} +type WidgetEntry = WidgetMediaEntry | WidgetParagraph +function parseWidgets(entries?: Array): Array | 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 { + 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(query) + + return blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost) +} + +interface FetchBlogPostOptions { + slug: string + preview: boolean +} +export async function fetchBlogPost({ slug, preview }: FetchBlogPostOptions): Promise { + const contentful = contentfulClient({ preview }) + + const blogPostsResult = await contentful.getEntries({ + content_type: 'blogPost', + 'fields.slug': slug, + include: 2, + }) + + return parseContentfulBlogPost(blogPostsResult.items[0]) +} \ No newline at end of file diff --git a/src/lib/contentful/blogPostsCategories.ts b/src/lib/contentful/blogPostsCategories.ts new file mode 100644 index 0000000..84006a3 --- /dev/null +++ b/src/lib/contentful/blogPostsCategories.ts @@ -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 + + + +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 { + const contentful = contentfulClient({ preview }) + + const results = await contentful.getEntries({ + content_type: 'blogPostCategory', + order: ['fields.title'], + }) + + return results.items.map((entry) => parseContentfulBlogPostCategory(entry) as BlogPostCategory) +} diff --git a/src/lib/contentful/contentImage.ts b/src/lib/contentful/contentImage.ts new file mode 100644 index 0000000..3048548 --- /dev/null +++ b/src/lib/contentful/contentImage.ts @@ -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 | { 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, + } +} \ No newline at end of file diff --git a/src/lib/contentful/contentfulClient.ts b/src/lib/contentful/contentfulClient.ts new file mode 100644 index 0000000..61fcb2e --- /dev/null +++ b/src/lib/contentful/contentfulClient.ts @@ -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 +} \ No newline at end of file diff --git a/src/types/author.ts b/src/types/author.ts new file mode 100644 index 0000000..05c7eac --- /dev/null +++ b/src/types/author.ts @@ -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 +export type AuthorEntry = Entry< + AuthorSkeleton, + Modifiers, + Locales +> \ No newline at end of file diff --git a/src/types/blogPost.ts b/src/types/blogPost.ts new file mode 100644 index 0000000..c4776c4 --- /dev/null +++ b/src/types/blogPost.ts @@ -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 +} + +export interface BlogPostFields extends BlogPostFields{ + body: Array +} + +export interface BlogPost { + title: string + slug: string + excerpt: string + listImage: ContentImage | null + author: Author | null + category: string + createdAt: string + body: Array +} + +export type BlogPostSkeleton = EntrySkeletonType +export type BlogPostEntry = Entry< + BlogPostSkeleton, + Modifiers, + Locales +> \ No newline at end of file diff --git a/src/types/blogPostCategory.ts b/src/types/blogPostCategory.ts new file mode 100644 index 0000000..65567df --- /dev/null +++ b/src/types/blogPostCategory.ts @@ -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 +export type BlogPostCategoryEntry = Entry< + BlogPostCategorySkeleton, + Modifiers, + Locales +> \ No newline at end of file diff --git a/src/types/blogWidgets/widgetMedia.ts b/src/types/blogWidgets/widgetMedia.ts new file mode 100644 index 0000000..172ae23 --- /dev/null +++ b/src/types/blogWidgets/widgetMedia.ts @@ -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 +export type WidgetMediaEntry = Entry< + WidgetMediaSkeleton, + Modifiers, + Locales +> \ No newline at end of file diff --git a/src/types/blogWidgets/widgetParagraph.ts b/src/types/blogWidgets/widgetParagraph.ts new file mode 100644 index 0000000..0d363cd --- /dev/null +++ b/src/types/blogWidgets/widgetParagraph.ts @@ -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 +export type WidgetParagraphEntry = Entry< + WidgetParagraphSkeleton, + Modifiers, + Locales +> \ No newline at end of file