Skip to content

Commit

Permalink
[#362]: Posts translates (#364)
Browse files Browse the repository at this point in the history
* [#362]: Posts translates

* Fixes after refactoring

* [#362]: Posts translates
  • Loading branch information
Themezv authored Jun 16, 2023
1 parent 7d4dae1 commit 600b5c5
Show file tree
Hide file tree
Showing 18 changed files with 1,208 additions and 631 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module.exports = {
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/quotes': ['error', 'single'],
'@typescript-eslint/quotes': ['error', 'single', { avoidEscape: true }],
'@typescript-eslint/type-annotation-spacing': 'off',
'@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
Expand Down
13 changes: 13 additions & 0 deletions apps/blog/content/what-is-memebattle.en.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: 'What is MemeBattle?'
publishedAt: '2023-05-28'
summary: "Post about MemeBattle. How it's started and what is MemeBattle today"
image: '/content-images/what-is-memebattle/memeBattle-logo.svg'
tags: ['MemeBattle']
---

## Started


* [GitHub](https://github.com/MemeBattle)
* [Linkedin](https://www.linkedin.com/company/memebattle/)
File renamed without changes.
12 changes: 9 additions & 3 deletions apps/blog/contentlayer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const Heading = defineNestedType(() => ({
}))

/**
*
* @param {string} fileName
* @return {string} language
*/
Expand All @@ -21,19 +20,26 @@ const extractFileLanguage = fileName => {
return fileNameParts.length > 2 ? fileNameParts[1] : 'en'
}

/**
* @param {string} rawFileName
* @return {string} language
*/
const extractSlug = rawFileName => rawFileName.split('.')[0]

/** @type {import('contentlayer/source-files').ComputedFields} */
const computedFields = {
slug: {
type: 'string',
resolve: doc => doc._raw.flattenedPath,
resolve: doc => extractSlug(doc._raw.flattenedPath),
},
toc: {
type: 'list',
of: Heading,
resolve: () => [{ level: 'h1', text: 'text' }],
},
lang: {
type: 'string',
type: 'enum',
options: ['en', 'ru'],
required: true,
resolve: doc => extractFileLanguage(doc._raw.sourceFileName),
},
Expand Down
10 changes: 8 additions & 2 deletions apps/blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"build": "next build",
"start": "next start",
"generate": "contentlayer build",
"ts-check": "yarn build && tsc --noEmit"
"ts-check": "yarn build && tsc --noEmit",
"test": "vitest",
"test:ci": "vitest run"
},
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.4",
Expand All @@ -28,9 +30,13 @@
"@types/node": "^18.14.0",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-v8": "^0.32.0",
"autoprefixer": "^10.4.14",
"jsdom": "^22.1.0",
"postcss": "^8.4.21",
"tailwindcss": "^3.3.1",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"vitest": "^0.32.0"
}
}
1 change: 1 addition & 0 deletions apps/blog/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function generateMetadata({ params }: { params: RootLayoutParams })
const { t } = await useTranslation(params.locale)

return {
metadataBase: new URL('https://blog.mems.fun'),
title: {
default: t('main.title'),
template: `%s | ${t('main.title')}`,
Expand Down
20 changes: 19 additions & 1 deletion apps/blog/src/app/[locale]/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { notFound } from 'next/navigation'
import type { Metadata, Route } from 'next'

import { Mdx } from '../../../../components/Mdx'
import type { BlogPost } from 'contentlayer/generated'
import { allBlogPosts } from 'contentlayer/generated'
Expand All @@ -7,6 +9,9 @@ import type { Language } from '../../../../i18n/i18n.settings'
import { ChipsRow } from '../../../../components/ChipsRow'
import { formatDate } from '../../../../utils/formatDate'

import { isPostShouldBePickedByLocale } from '../_utils/isPostShouldBePickedByLocale'
import { allBlogPostsWithTranslates } from '../_content'

interface BlogProps {
params: {
slug: string
Expand All @@ -20,8 +25,21 @@ export async function generateStaticParams() {
}))
}

export const generateMetadata = ({ params }: BlogProps): Metadata => {
const blogPost = allBlogPostsWithTranslates.find(post => post.slug === params.slug && isPostShouldBePickedByLocale(post, params.locale))

return {
alternates: {
languages: Object.keys(blogPost?.translates || {}).reduce<{ [key in Language]?: Route }>(
(acc, postLocale) => ({ ...acc, [postLocale]: `/${postLocale}/posts/${params.slug}` }),
{},
),
},
}
}

export default function Post({ params }: BlogProps) {
const post = allBlogPosts.find((post: BlogPost) => post.slug === params.slug)
const post = allBlogPostsWithTranslates.find(post => post.slug === params.slug && isPostShouldBePickedByLocale(post, params.locale))

if (!post) {
notFound()
Expand Down
22 changes: 22 additions & 0 deletions apps/blog/src/app/[locale]/posts/_content/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { BlogPost } from 'contentlayer/generated'
import { allBlogPosts } from 'contentlayer/generated'
import type { Language } from '../../../../i18n/i18n.settings'

export { allBlogPosts }
export type BlogPostWithTranslates = BlogPost & { translates: { [key in Language]?: BlogPost } }

export const uniqTags = [
...allBlogPosts.reduce<Set<string>>((acc, { tags = [] }) => {
tags.forEach(tag => {
acc.add(tag)
})
return acc
}, new Set<string>()),
]

export const allBlogPostsWithTranslates: BlogPostWithTranslates[] = allBlogPosts.map((blogPost, index, blogPosts) => ({
...blogPost,
translates: blogPosts
.filter(({ slug }) => blogPost.slug === slug)
.reduce((translatesAcc, blogPost) => ({ ...translatesAcc, [blogPost.lang]: blogPost }), {}),
}))
113 changes: 113 additions & 0 deletions apps/blog/src/app/[locale]/posts/_utils/filterBlogPosts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { filterBlogPosts } from './filterBlogPosts'
import type { BlogPostWithTranslates } from '../_content'

const posts: BlogPostWithTranslates[] = [
{
title: 'Заголовок поста 1',
publishedAt: '2023-05-28T00:00:00.000Z',
summary: 'Первый пост в блоге',
tags: ['тэг1'],
image: '/content-images/what-is-memebattle/memeBattle-logo.svg',
body: {
raw: 'раз два три',
code: '',
},
_id: 'first.ru.mdx',
_raw: {
sourceFilePath: 'first.ru.mdx',
sourceFileName: 'first.ru.mdx',
sourceFileDir: '.',
contentType: 'mdx',
flattenedPath: 'first.ru',
},
type: 'BlogPost',
slug: 'first',
toc: [
{
level: 'h1',
text: 'text',
},
],
lang: 'ru',
translates: {},
},
{
title: 'Заголовок поста 2',
publishedAt: '2023-05-28T00:00:00.000Z',
summary: 'Тезисное содержание поста 2',
tags: [],
image: '/content-images/what-is-memebattle/memeBattle-logo.svg',
body: {
raw: 'какой-то текст',
code: '',
},
_id: 'some-id.mdx',
_raw: {
sourceFilePath: 'some-id.mdx',
sourceFileName: 'some-id.mdx',
sourceFileDir: '.',
contentType: 'mdx',
flattenedPath: 'some-id',
},
type: 'BlogPost',
slug: 'some-id',
toc: [
{
level: 'h1',
text: 'text',
},
],
lang: 'ru',
translates: {},
},
{
title: 'First post title',
publishedAt: '2023-05-28T00:00:00.000Z',
summary: 'First blog post',
tags: ['tag1'],
image: '/content-images/what-is-memebattle/memeBattle-logo.svg',
body: {
raw: 'one two three',
code: '',
},
_id: 'first.en.mdx',
_raw: {
sourceFilePath: 'first.en.mdx',
sourceFileName: 'first.en.mdx',
sourceFileDir: '.',
contentType: 'mdx',
flattenedPath: 'first.en',
},
type: 'BlogPost',
slug: 'first',
toc: [
{
level: 'h1',
text: 'text',
},
],
lang: 'en',
translates: {},
},
]

posts[0].translates = { en: posts[2] }
posts[2].translates = { ru: posts[1] }

describe('filterBlogPosts', () => {
it('Should return all (uniq by locale) posts if search and tags empty', () => {
expect(filterBlogPosts(posts, 'ru')).toEqual([posts[0], posts[1]])
})

it("Should return empty posts if posts doesn't contain words from search", () => {
expect(filterBlogPosts(posts, 'ru', 'some query')).toEqual([])
})

it('Should return filtered post that contains words from search', () => {
expect(filterBlogPosts(posts, 'ru', 'раз')).toEqual([posts[0]])
})

it('Should return filtered post that contains words from search', () => {
expect(filterBlogPosts(posts, 'ru', 'раз')).toEqual([posts[0]])
})
})
24 changes: 24 additions & 0 deletions apps/blog/src/app/[locale]/posts/_utils/filterBlogPosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Language } from '../../../../i18n/i18n.settings'
import { isPostShouldBePickedByLocale } from './isPostShouldBePickedByLocale'
import type { BlogPostWithTranslates } from '../_content'

export function filterBlogPosts(blogPosts: BlogPostWithTranslates[], locale: Language, search = '', tags: string[] = []): BlogPostWithTranslates[] {
const keywords = search
.toLowerCase()
.split(' ')
.filter(part => part !== '')

const filteredByLocale = blogPosts.filter(blogPost => isPostShouldBePickedByLocale(blogPost, locale))

const filteredByTag = tags.length > 0 ? filteredByLocale.filter(blogPost => blogPost.tags?.some(tag => tags.includes(tag))) : filteredByLocale

if (keywords.length === 0) {
return filteredByTag
}

return filteredByTag.filter(blogPost => {
const words = (blogPost.title + ' ' + blogPost.summary + ' ' + blogPost.body.raw).toLowerCase().split(' ')

return keywords.every(keyWord => words.some(word => word.startsWith(keyWord)))
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { isPostShouldBePickedByLocale } from './isPostShouldBePickedByLocale'

describe('isPostShouldBePickedByLocale', () => {
it('Should return true if post in selected locale', () => {
expect(isPostShouldBePickedByLocale({ slug: 'first', lang: 'ru', translates: {} }, 'ru')).toEqual(true)
})

it('Should return false if post not in selected locale', () => {
expect(isPostShouldBePickedByLocale({ slug: 'first', lang: 'ru', translates: {} }, 'en')).toEqual(false)
})

it('Should return true if post in fallback locale and not exist in current', () => {
expect(isPostShouldBePickedByLocale({ slug: 'first', lang: 'en', translates: {} }, 'ru')).toEqual(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { BlogPostWithTranslates } from '../_content'
import type { Language } from '../../../../i18n/i18n.settings'
import { fallbackLanguage } from '../../../../i18n/i18n.settings'

export const isPostShouldBePickedByLocale = (post: Pick<BlogPostWithTranslates, 'slug' | 'lang' | 'translates'>, locale: Language): boolean => {
/**
* Post in current locale should be picked
*/
if (post.lang === locale) {
return true
}

/**
* Post in fallback locale should be picked if it doesn't exist in current
*/
return post.lang === fallbackLanguage && !post.translates[locale]
}
38 changes: 5 additions & 33 deletions apps/blog/src/app/[locale]/posts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BlogPost } from 'contentlayer/generated'
import { allBlogPosts } from 'contentlayer/generated'
import Link from 'next/link'
import { allBlogPostsWithTranslates, uniqTags } from './_content'
import { filterBlogPosts } from './_utils/filterBlogPosts'
import { useTranslation } from '../../../i18n'
import type { Language } from '../../../i18n/i18n.settings'
import { SearchInput } from '../../../components/SearchInput'
Expand All @@ -16,34 +16,6 @@ function SearchLoader() {
return <div className="rounded-md shadow-sm h-16 border-0 text-gray-900" />
}

const uniqTags = [
...allBlogPosts.reduce<Set<string>>((acc, { tags = [] }) => {
tags.forEach(tag => {
acc.add(tag)
})
return acc
}, new Set<string>()),
]

function filterBlogPosts(blogPosts: BlogPost[], search = '', tags: string[] = []): BlogPost[] {
const keywords = search
.toLowerCase()
.split(' ')
.filter(part => part !== '')

const filteredByTag = tags.length > 0 ? blogPosts.filter(blogPost => blogPost.tags?.some(tag => tags.includes(tag))) : blogPosts

if (keywords.length === 0) {
return filteredByTag
}

return filteredByTag.filter(blogPost => {
const words = (blogPost.title + ' ' + blogPost.summary + ' ' + blogPost.body.code).toLowerCase().split(' ')

return keywords.every(keyWord => words.some(word => word.startsWith(keyWord)))
})
}

function searchParamsSearchFormatter(searchQuery: string | string[] | undefined): string | undefined {
if (Array.isArray(searchQuery)) {
return searchQuery.join(' ')
Expand All @@ -70,8 +42,8 @@ export default async function BlogPage({
const searchQueryTags = searchParamsTagsFormatter(searchParams.tags)
const searchQuerySearch = searchParamsSearchFormatter(searchParams.search)

const filteredPosts = filterBlogPosts(allBlogPosts, searchQuerySearch, searchQueryTags)
.sort((a: BlogPost, b: BlogPost) => {
const filteredPosts = filterBlogPosts(allBlogPostsWithTranslates, locale, searchQuerySearch, searchQueryTags)
.sort((a, b) => {
if (new Date(a.publishedAt) > new Date(b.publishedAt)) {
return -1
}
Expand Down Expand Up @@ -99,7 +71,7 @@ export default async function BlogPage({
</Suspense>
</nav>
<main className="flex flex-col space-y-6 max-w-3xl w-full">
{filteredPosts.map((post: BlogPost) => (
{filteredPosts.map(post => (
<Link key={post.slug} href={`/${locale}/posts/${post.slug}`} className="group">
<PostsListItem
tags={post.tags?.map(tag => ({ text: tag, isActive: searchQueryTags?.includes(tag) || false }))}
Expand Down
Loading

1 comment on commit 600b5c5

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for apps/ligretto-gameplay-backend

St.
Category Percentage Covered / Total
🔴 Statements 48.63% 373/767
🔴 Branches 24.53% 26/106
🔴 Functions 26.07% 55/211
🔴 Lines 46.27% 310/670

Test suite run success

12 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from 600b5c5

Please sign in to comment.