Compare commits

..

3 Commits

10 changed files with 264 additions and 175 deletions

6
.env
View File

@ -1,6 +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
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE

View File

@ -6,11 +6,6 @@ import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/
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
}
@ -19,11 +14,19 @@ interface BlogPostPageProps {
params: BlogPostPageParams
}
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
const blogPosts = await fetchBlogPosts({ preview: false })
export async function generateMetadata({ params }: BlogPostPageProps, parent: ResolvingMetadata): Promise<Metadata> {
const blogPost = await fetchBlogPost({ slug: params.slug, preview: draftMode().isEnabled })
return blogPosts.map((post) => ({ slug: post.slug }))
if (!blogPost) {
return notFound()
}
return {
title: blogPost.title
}
}
function renderWidget (widget: Widget) {
switch (widget.type){
case 'widgetParagraph':

View File

@ -5,6 +5,7 @@ import {unstable_setRequestLocale} from "next-intl/server";
import Link from "next/link";
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
import {BlogPosts} from "../../../../../components/BlogPosts/BlogPosts";
export const metadata: Metadata = {
title: 'Bbuddy - Blog',
@ -19,83 +20,10 @@ interface BlogPostPageProps {
params: BlogPostPageParams
}
export default async function Blog({params}: { params: BlogPostPageParams }) {
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searhParams?: {page: number} }) {
unstable_setRequestLocale(params.locale);
const data = await fetchBlogPosts({ preview: draftMode().isEnabled, locale: params.locale, category: params.slug })
const cats = await fetchBlogPostCategories(false)
const page = searchParams.page || undefined
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>
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
);
}

View File

@ -5,91 +5,39 @@ 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'
};
import {CustomPagination} from "../../../components/view/CustomPagination";
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
interface BlogPostPageParams {
slug: string
}
export default async function Blog({ params: { locale } }: { params: { locale: string } }) {
interface BlogPostPageProps {
params: BlogPostPageParams
}
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
const blogPosts = await fetchBlogPosts({ preview: false })
return blogPosts.data.map((post) => ({ slug: post.slug }))
}
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searhParams?: {page: number} }) {
unstable_setRequestLocale(locale);
const data = await fetchBlogPosts(false, locale)
const cats = await fetchBlogPostCategories(false)
const pageSize = DEFAULT_PAGE_SIZE
const page = searchParams.page || undefined
// BlogPosts('/'+locale+'/blog/', locale, pageSize)
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 href={'category/'+cat.slug} className="filter-item">{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={'blog'+i} className="list-sidebar__item">
<Link href={`${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>
<BlogPosts
basePath={'/'+locale+'/blog/'}
locale={locale}
pageSize={pageSize}
page={page}
>
</BlogPosts>
);
}

27
src/app/sitemap.jsx Normal file
View File

@ -0,0 +1,27 @@
import {fetchBlogPosts} from "../lib/contentful/blogPosts";
export default async function sitemap() {
const paths = [
{
url: process.env.NEXT_PUBLIC_HOST,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1
}
]
const blogPosts = await fetchBlogPosts({ preview: false })
blogPosts.data.forEach((item) => {
paths.push({
url: `${process.env.NEXT_PUBLIC_HOST}${item.slug}`,
lastModified: item.createdAt.split('T')[0],
changeFrequency: 'daily',
priority: '1.0'
})
})
return paths
}

View File

@ -0,0 +1,35 @@
'use client';
import React, {useState} from "react";
import {Languages} from "../../types/tags";
import {BlogPostCategory} from "../../types/blogPostCategory";
import Link from "next/link";
type Props = {
languages?: Languages;
basePath: string;
locale: string;
cats: BlogPostCategory[],
slug: string
};
export const BlogPostCategories = ({ basePath = '/', cats = [], slug = '' }: Props) => {
const [currentCat, setCurrentCat] = useState<String>(slug);
return (
<div className="b-news__filter ">
<div className="b-inner">
<div className="wrap-filter">
{
cats.map((cat, i)=>(
<Link
href={ basePath+'category/'+cat.slug} key={'blogCat'+i}
className={"filter-item"+(cat.slug === currentCat ? ' active' : '')}
>
{cat.title}
</Link>
))
}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,53 @@
import React from 'react';
import { DEFAULT_PAGE_SIZE } from '../../constants/common';
import {getLanguages} from "../../actions/tags";
import {fetchBlogPosts} from "../../lib/contentful/blogPosts";
import {fetchBlogPostCategories} from "../../lib/contentful/blogPostsCategories";
import {BlogPostsList} from "./BlogPostsList";
import {BlogPostCategories} from "./BlogPostCategories";
type PostsProps = {
basePath: string;
locale: string;
pageSize?: number;
currentCat: string;
page?: number
};
export const BlogPosts = async ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE, currentCat = '', page = 1 }: PostsProps) => {
const languages = await getLanguages(locale);
const {data, total} = await fetchBlogPosts({preview: false, category: currentCat, page: page})
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>
<BlogPostCategories
slug={currentCat}
cats={cats}
basePath={basePath}
locale={locale}
/>
<BlogPostsList
data={data}
total={total}
basePath={basePath}
locale={locale}
/>
</div>
)
}

View File

@ -0,0 +1,79 @@
'use client';
import React from 'react';
import { DEFAULT_PAGE_SIZE } from '../../constants/common';
import {Languages, SearchData} from "../../types/tags";
import {BlogPost} from "../../types/blogPost";
import Link from "next/link";
import {CustomPagination} from "../view/CustomPagination";
type Props = {
searchData?: SearchData;
languages?: Languages;
basePath: string;
locale: string;
data: BlogPost[],
total: number
};
export const BlogPostsList = ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE, data = [], total= 0 }: Props) => {
const currentPage = 1
const onChangePage = (page: number) => {
router.push(page === 1 ? basePath : basePath+'?page='+page);
};
return (
<div className="b-news__result-list">
<div className="b-inner">
<div className="news-list">
{data.map((item, i) => (
<li key={'blog'+i} className="list-sidebar__item">
<Link href={`${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>
{total > pageSize && (
<CustomPagination
total={total}
pageSize={pageSize}
onChange={onChangePage}
current={currentPage}
/>)}
</div>
</div>
)
}

View File

@ -8,7 +8,9 @@ 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";
import {DEFAULT_PAGE_SIZE} from "../../constants/common";
const pageSize = DEFAULT_PAGE_SIZE
type PostEntry = BlogPostEntry<undefined, string>//Entry<BlogPostSkeleton, undefined, string>
type widgetEnum = WidgetParagraph | WidgetMedia
export type Widget = {
@ -65,8 +67,12 @@ interface FetchBlogPostsOptions {
preview: boolean
local?: string
category?: string
page?: number
}
export async function fetchBlogPosts({ preview, category }: FetchBlogPostsOptions): Promise<BlogPost[]> {
export async function fetchBlogPosts({ preview, category, page }: FetchBlogPostsOptions): Promise<{
total: number;
data: BlogPost[]
}> {
const contentful = contentfulClient({ preview })
const query = {
content_type: 'blogPost',
@ -77,9 +83,19 @@ export async function fetchBlogPosts({ preview, category }: FetchBlogPostsOption
query['fields.category.fields.slug'] = category
query['fields.category.sys.contentType.sys.id']='blogPostCategory'
}
if(page){
query['limit'] = pageSize
query['skip'] = pageSize * (page - 1)
}
const blogPostsResult = await contentful.getEntries<BlogPostSkeleton>(query)
return blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost)
const data = blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost)
return {
total: blogPostsResult.total,
data
}
}
interface FetchBlogPostOptions {

View File

@ -1,19 +1,19 @@
import { createClient } from 'contentful'
const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_PREVIEW_ACCESS_TOKEN } = process.env
const { NEXT_PUBLIC_CONTENTFUL_SPACE_ID, NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN, NEXT_PUBLIC_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!,
space: NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
accessToken: NEXT_PUBLIC_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!,
space: NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
accessToken: NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
host: 'preview.contentful.com',
})