author-page-wip

This commit is contained in:
Untone 2024-07-13 10:01:41 +03:00
parent 25d217389b
commit b7e775eeea
10 changed files with 135 additions and 169 deletions

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ target
.output .output
.vinxi .vinxi
*.pem

57
README.en.md Normal file
View File

@ -0,0 +1,57 @@
## Development setup recommendations
### How to start
Use `bun i`, `npm i`, `pnpm i` or `yarn` to install packages. Then generate cert and key file for devserver with `mkcert localhost`.
### Config of variables
- Use `.env` file to setup your own development environment
- Env vars with prefix `PUBLIC_` are widely used in `/src/utils/config.ts`
### Useful commands
run checks, fix styles, imports, formatting and autofixable linting errors:
```
bun run typecheck
bun run fix
```
## End-to-End (E2E) Tests
This directory contains end-to-end tests. These tests are written using [Playwright](https://playwright.dev/)
### Structure
- `/tests/*`: This directory contains the test files.
- `/playwright.config.ts`: This is the configuration file for Playwright.
### Getting Started
Follow these steps:
1. **Install dependencies**: Run `npm run e2e:install` to install the necessary dependencies for running the tests.
2. **Run the tests**: After using `npm run e2e:tests`.
### Additional Information
If workers is no needed use:
- `npx playwright test --project=webkit --workers 4`
For more information on how to write tests using Playwright - [Playwright documentation](https://playwright.dev/docs/intro).
### 🚀 Tests in CI Mode
Tests are executed within a GitHub workflow. We organize our tests into two main directories:
- `tests`: Contains tests that do not require authentication.
- `tests-with-auth`: Houses tests that interact with authenticated parts of the application.
🔧 **Configuration:**
Playwright is configured to utilize the `BASE_URL` environment variable. Ensure this is properly set in your CI configuration to point to the correct environment.
📝 **Note:**
After pages have been adjusted to work with authentication, all tests should be moved to the `tests` directory to streamline the testing process.

View File

@ -1,63 +1,59 @@
## How to start ## Рекомендации по настройке разработки
Use Bun to manage packages. ### Как начать
``` Используйте `bun i`, `npm i`, `pnpm i` или `yarn`, чтобы установить пакеты. Затем сгенерируйте сертификат и файл ключа для devserver с помощью `mkcert localhost`.
bun i
``` ### Настройка переменных
- Используйте файл `.env` для настройки переменных собственной среды разработки.
- Переменные окружения с префиксом `PUBLIC_` широко используются в `/src/utils/config.ts`.
### Полезные команды
Запуск проверки соответствия типов и автоматически исправить ошибки стилей, порядок импорта, форматирование:
## Useful commands
run checks
``` ```
bun run typecheck bun run typecheck
```
fix styles, imports, formatting and autofixable linting errors:
```
bun run fix bun run fix
``` ```
## Config of variables
- All vars are already in place and wroted in ## End-to-End (E2E) тесты
```
/src/utils/config.ts
```
# End-to-End (E2E) Tests End-to-end тесты написаны с использованием [Playwright](https://playwright.dev/).
This directory contains end-to-end tests. These tests are written using [Playwright](https://playwright.dev/) ### Структура
## Structure - `/tests/*`: содержит файлы тестов
- `/playwright.config.ts`: конфиг для Playwright
- `/tests/*`: This directory contains the test files. ### Начало работы
- `/playwright.config.ts`: This is the configuration file for Playwright.
## Getting Started Следуйте этим шагам:
Follow these steps: 1. **Установите зависимости**: Запустите `npm run e2e:install`, чтобы установить необходимые зависимости для выполнения тестов.
1. **Install dependencies**: Run `pnpm e2e:install` to install the necessary dependencies for running the tests. 2. **Запустите тесты**: После установки зависимостей используйте `npm run e2e:tests`.
2. **Run the tests**: After using `pnpm e2e:tests`. ### Дополнительная информация
## Additional Information Для параллельного исполнения:
If workers is no needed use:
- `npx playwright test --project=webkit --workers 4` - `npx playwright test --project=webkit --workers 4`
For more information on how to write tests using Playwright - [Playwright documentation](https://playwright.dev/docs/intro). Для получения дополнительной информации о написании тестов с использованием Playwright - [Документация Playwright](https://playwright.dev/docs/intro).
## 🚀 Tests in CI Mode ### 🚀 Тесты в режиме CI
Tests are executed within a GitHub workflow. We organize our tests into two main directories: Тесты выполняются в рамках GitHub workflow. Мы организуем наши тесты в две основные директории:
- `tests`: Contains tests that do not require authentication. - `tests`: Содержит тесты, которые не требуют аутентификации.
- `tests-with-auth`: Houses tests that interact with authenticated parts of the application. - `tests-with-auth`: Содержит тесты, которые взаимодействуют с аутентифицированными частями приложения.
🔧 **Configuration:** 🔧 **Конфигурация:**
Playwright is configured to utilize the `BASE_URL` environment variable. Ensure this is properly set in your CI configuration to point to the correct environment. Playwright настроен на использование переменной окружения `BASE_URL`. Убедитесь, что она правильно установлена в вашей конфигурации CI для указания на правильную среду.
📝 **Note:** 📝 **Примечание:**
After pages have been adjusted to work with authentication, all tests should be moved to the `tests` directory to streamline the testing process. После того как страницы были настроены для работы с аутентификацией, все тесты должны быть перемещены в директорию `tests` для упрощения процесса тестирования.

View File

@ -1,4 +1,6 @@
import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config' import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config'
import { visualizer } from "rollup-plugin-visualizer"
import mkcert from 'vite-plugin-mkcert'
import { nodePolyfills } from 'vite-plugin-node-polyfills' import { nodePolyfills } from 'vite-plugin-node-polyfills'
import sassDts from 'vite-plugin-sass-dts' import sassDts from 'vite-plugin-sass-dts'
@ -20,6 +22,7 @@ export default defineConfig({
vite: { vite: {
envPrefix: 'PUBLIC_', envPrefix: 'PUBLIC_',
plugins: [ plugins: [
mkcert(),
nodePolyfills({ nodePolyfills({
include: ['path', 'stream', 'util'], include: ['path', 'stream', 'util'],
exclude: ['http'], exclude: ['http'],
@ -43,7 +46,10 @@ export default defineConfig({
}, },
build: { build: {
chunkSizeWarningLimit: 1024, chunkSizeWarningLimit: 1024,
target: 'esnext' target: 'esnext',
rollupOptions: {
plugins: [visualizer(), ]
}
}, },
server: { server: {
https: true https: true

View File

@ -89,6 +89,7 @@
"prosemirror-history": "^1.4.1", "prosemirror-history": "^1.4.1",
"prosemirror-trailing-node": "^2.0.8", "prosemirror-trailing-node": "^2.0.8",
"prosemirror-view": "^1.33.8", "prosemirror-view": "^1.33.8",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "1.76.0", "sass": "1.76.0",
"solid-js": "^1.8.18", "solid-js": "^1.8.18",
"solid-popper": "^0.3.0", "solid-popper": "^0.3.0",
@ -106,6 +107,7 @@
"typograf": "^7.4.1", "typograf": "^7.4.1",
"uniqolor": "^1.1.1", "uniqolor": "^1.1.1",
"vinxi": "^0.3.14", "vinxi": "^0.3.14",
"vite-plugin-mkcert": "^1.17.5",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-sass-dts": "^1.3.24", "vite-plugin-sass-dts": "^1.3.24",
"y-prosemirror": "1.2.9", "y-prosemirror": "1.2.9",

View File

@ -28,35 +28,42 @@ import styles from './Author.module.scss'
type Props = { type Props = {
authorSlug: string authorSlug: string
selectedTab: string
shouts?: Shout[] shouts?: Shout[]
author?: Author author?: Author
topics?: Topic[] topics?: Topic[]
selectedTab: string
} }
export const PRERENDERED_ARTICLES_COUNT = 12 export const PRERENDERED_ARTICLES_COUNT = 12
const LOAD_MORE_PAGE_SIZE = 9 const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: Props) => { export const AuthorView = (props: Props) => {
console.debug('[components.AuthorView] reactive context init...') // contexts
const { t } = useLocalize() const { t } = useLocalize()
const params = useParams()
const { followers: myFollowers, follows: myFollows } = useFollowing()
const { session } = useSession()
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const [authorSlug, setSlug] = createSignal(props.authorSlug)
const { sortedFeed } = useFeed()
const loc = useLocation() const loc = useLocation()
const params = useParams()
const { session } = useSession()
const { query } = useGraphQL()
const { sortedFeed } = useFeed()
const { loadAuthor, authorsEntities } = useAuthors()
const { followers: myFollowers, follows: myFollows } = useFollowing()
// signals
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
const { loadAuthor, authorsEntities } = useAuthors()
const [author, setAuthor] = createSignal<Author>() const [author, setAuthor] = createSignal<Author>()
const [followers, setFollowers] = createSignal<Author[]>([] as Author[]) const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
const [following, changeFollowing] = createSignal<Array<Author | Topic>>([] as Array<Author | Topic>) // flat AuthorFollowsResult const [following, changeFollowing] = createSignal<Array<Author | Topic>>([] as Array<Author | Topic>) // flat AuthorFollowsResult
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
const [commented, setCommented] = createSignal<Reaction[]>([]) const [commented, setCommented] = createSignal<Reaction[]>([])
const { query } = useGraphQL()
// derivatives
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
// fx
// пагинация загрузки ленты постов // пагинация загрузки ленты постов
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
@ -74,6 +81,7 @@ export const AuthorView = (props: Props) => {
const [isFetching, setIsFetching] = createSignal(false) const [isFetching, setIsFetching] = createSignal(false)
createEffect( createEffect(
on([() => session()?.user?.app_data?.profile, () => props.authorSlug || ''], async ([me, slug]) => { on([() => session()?.user?.app_data?.profile, () => props.authorSlug || ''], async ([me, slug]) => {
console.debug('check if my profile')
const my = slug && me?.slug === slug const my = slug && me?.slug === slug
if (my) { if (my) {
console.debug('[Author] my profile precached') console.debug('[Author] my profile precached')
@ -84,29 +92,27 @@ export const AuthorView = (props: Props) => {
} }
} else if (slug && !isFetching()) { } else if (slug && !isFetching()) {
setIsFetching(true) setIsFetching(true)
setSlug(slug)
await loadAuthor({ slug }) await loadAuthor({ slug })
setIsFetching(false) // Сброс состояния загрузки после завершения setIsFetching(false) // Сброс состояния загрузки после завершения
} }
}) }, {defer: true})
) )
// 2 // догружает подписки автора // 2 // догружает подписки автора
createEffect( createEffect(
on( on(
[followers, () => props.author || authorsEntities()[authorSlug()]], () => authorsEntities()[props.author?.slug || props.authorSlug || ''],
async ([current, found]) => { async (found) => {
if (current) return
if (!found) return if (!found) return
setAuthor(found) setAuthor(found)
console.info(`[Author] profile for @${authorSlug()} fetched`) console.info(`[Author] profile for @${found.slug} fetched`)
const followsResp = await query(getAuthorFollowsQuery, { slug: authorSlug() }).toPromise() const followsResp = await query(getAuthorFollowsQuery, { slug: found.slug }).toPromise()
const follows = followsResp?.data?.get_author_followers || {} const follows = followsResp?.data?.get_author_followers || {}
changeFollowing([...(follows?.authors || []), ...(follows?.topics || [])]) changeFollowing([...(follows?.authors || []), ...(follows?.topics || [])])
console.info(`[Author] follows for @${authorSlug()} fetched`) console.info(`[Author] follows for @${found.slug} fetched`)
const followersResp = await query(getAuthorFollowersQuery, { slug: authorSlug() }).toPromise() const followersResp = await query(getAuthorFollowersQuery, { slug: found.slug }).toPromise()
setFollowers(followersResp?.data?.get_author_followers || []) setFollowers(followersResp?.data?.get_author_followers || [])
console.info(`[Author] followers for @${authorSlug()} fetched`) console.info(`[Author] followers for @${found.slug} fetched`)
setIsFetching(false) setIsFetching(false)
}, },
{ defer: true } { defer: true }
@ -120,33 +126,32 @@ export const AuthorView = (props: Props) => {
async (profile: Author) => { async (profile: Author) => {
if (!commented() && profile) { if (!commented() && profile) {
await loadMore() await loadMore()
const commentsFetcher = loadReactions({ const commentsFetcher = loadReactions({
by: { comment: true, created_by: profile.id } by: { comment: true, created_by: profile.id }
}) })
const ccc = await commentsFetcher() const ccc = await commentsFetcher()
if (ccc) setCommented((_) => ccc || []) if (ccc) setCommented((_) => ccc || [])
} }
} },
// { defer: true }, // { defer: true },
) )
) )
// event handlers
let bioContainerRef: HTMLDivElement let bioContainerRef: HTMLDivElement
let bioWrapperRef: HTMLDivElement let bioWrapperRef: HTMLDivElement
const checkBioHeight = () => { const checkBioHeight = () => {
console.debug('[AuthorView] mounted, checking bio height...')
if (bioContainerRef) { if (bioContainerRef) {
setShowExpandBioControl(bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight) setShowExpandBioControl(bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight)
} }
} }
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
const handleDeleteComment = (id: number) => { const handleDeleteComment = (id: number) => {
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id)) setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
} }
// on load
onMount(checkBioHeight) onMount(checkBioHeight)
return ( return (

View File

@ -95,6 +95,7 @@ export const AuthorsProvider = (props: { children: JSX.Element }) => {
const fetcher = await getAuthor(opts) const fetcher = await getAuthor(opts)
const author = await fetcher() const author = await fetcher()
if (author) addAuthor(author as Author) if (author) addAuthor(author as Author)
console.debug('Loaded author:', author)
} catch (error) { } catch (error) {
console.error('Error loading author:', error) console.error('Error loading author:', error)
throw error throw error

View File

@ -106,7 +106,7 @@ export const loadFollowersByTopic = (slug: string) => {
// TODO: paginate topic followers // TODO: paginate topic followers
return cache(async () => { return cache(async () => {
const resp = await defaultClient.query(loadFollowersByTopicQuery, { slug }).toPromise() const resp = await defaultClient.query(loadFollowersByTopicQuery, { slug }).toPromise()
const result = resp?.data?.load_authors_by const result = resp?.data?.get_topic_followers
if (result) return result as Author[] if (result) return result as Author[]
}, `topic-${slug}`) }, `topic-${slug}`)
} }

View File

@ -30,7 +30,7 @@ const fetchAllTopics = async () => {
} }
const fetchAuthor = async (slug: string) => { const fetchAuthor = async (slug: string) => {
const authorFetcher = loadAuthors({ by: { slug }, limit: 1 } as QueryLoad_Authors_ByArgs) const authorFetcher = loadAuthors({ by: { slug }, limit: 1, offset: 0 } as QueryLoad_Authors_ByArgs)
const aaa = await authorFetcher() const aaa = await authorFetcher()
return aaa?.[0] return aaa?.[0]
} }
@ -78,6 +78,8 @@ export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; to
? getImageUrl(author()?.pic || '', { width: 1200 }) ? getImageUrl(author()?.pic || '', { width: 1200 })
: getImageUrl('production/image/logo_image.png') : getImageUrl('production/image/logo_image.png')
) )
const selectedTab = createMemo(() => params.tab in ['followers', 'shouts'] ? params.tab : 'name')
return ( return (
<ErrorBoundary fallback={(_err) => <FourOuFourView />}> <ErrorBoundary fallback={(_err) => <FourOuFourView />}>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
@ -91,9 +93,9 @@ export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; to
<ReactionsProvider> <ReactionsProvider>
<AuthorView <AuthorView
author={author() as Author} author={author() as Author}
selectedTab={selectedTab()}
authorSlug={params.slug} authorSlug={params.slug}
shouts={articles() as Shout[]} shouts={articles() as Shout[]}
selectedTab={'shouts'}
topics={topics()} topics={topics()}
/> />
</ReactionsProvider> </ReactionsProvider>

View File

@ -1,104 +0,0 @@
import { RouteSectionProps, createAsync, useParams } from '@solidjs/router'
import { ErrorBoundary, Suspense, createEffect, createMemo } from 'solid-js'
import { AuthorView } from '~/components/Views/Author'
import { FourOuFourView } from '~/components/Views/FourOuFour'
import { Loading } from '~/components/_shared/Loading'
import { PageLayout } from '~/components/_shared/PageLayout'
import { useAuthors } from '~/context/authors'
import { useLocalize } from '~/context/localize'
import { ReactionsProvider } from '~/context/reactions'
import { loadAuthors, loadShouts, loadTopics } from '~/graphql/api/public'
import {
Author,
LoadShoutsOptions,
QueryLoad_Authors_ByArgs,
Shout,
Topic
} from '~/graphql/schema/core.gen'
import { getImageUrl } from '~/lib/getImageUrl'
import { SHOUTS_PER_PAGE } from '../../(home)'
const fetchAuthorShouts = async (slug: string, offset?: number) => {
const opts: LoadShoutsOptions = { filters: { author: slug }, limit: SHOUTS_PER_PAGE, offset }
const shoutsLoader = loadShouts(opts)
return await shoutsLoader()
}
const fetchAllTopics = async () => {
const topicsFetcher = loadTopics()
return await topicsFetcher()
}
const fetchAuthor = async (slug: string) => {
const authorFetcher = loadAuthors({ by: { slug }, limit: 1 } as QueryLoad_Authors_ByArgs)
const aaa = await authorFetcher()
return aaa?.[0]
}
export const route = {
load: async ({ params, location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => {
const offset: number = Number.parseInt(query.offset, 10)
const result = await fetchAuthorShouts(params.slug, offset)
return {
author: await fetchAuthor(params.slug),
shouts: result || [],
topics: await fetchAllTopics()
}
}
}
export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; topics: Topic[] }>) => {
const params = useParams()
const { addAuthor } = useAuthors()
const articles = createAsync(
async () => props.data.articles || (await fetchAuthorShouts(params.slug)) || []
)
const author = createAsync(async () => {
const a = props.data.author || (await fetchAuthor(params.slug))
addAuthor(a)
return a
})
const topics = createAsync(async () => props.data.topics || (await fetchAllTopics()))
const { t } = useLocalize()
const title = createMemo(() => `${author()?.name || ''}`)
createEffect(() => {
if (author()) {
console.debug('[routes] author/[slug] author loaded fx')
window?.gtag?.('event', 'page_view', {
page_title: author()?.name || '',
page_location: window?.location.href || '',
page_path: window?.location.pathname || ''
})
}
})
const cover = createMemo(() =>
author()?.pic
? getImageUrl(author()?.pic || '', { width: 1200 })
: getImageUrl('production/image/logo_image.png')
)
return (
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
<Suspense fallback={<Loading />}>
<PageLayout
title={`${t('Discours')} :: ${title()}`}
headerTitle={author()?.name || ''}
slug={author()?.slug}
desc={author()?.about || author()?.bio || ''}
cover={cover()}
>
<ReactionsProvider>
<AuthorView
author={author() as Author}
authorSlug={params.slug}
shouts={articles() as Shout[]}
selectedTab={params.tab}
topics={topics()}
/>
</ReactionsProvider>
</PageLayout>
</Suspense>
</ErrorBoundary>
)
}