diff --git a/.gitignore b/.gitignore index 9966a2a6..a8ed6c74 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ target .output .vinxi +*.pem diff --git a/README.en.md b/README.en.md new file mode 100644 index 00000000..89ebc8e9 --- /dev/null +++ b/README.en.md @@ -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. diff --git a/README.md b/README.md index d45e61c2..cd40c101 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,59 @@ -## How to start +## Рекомендации по настройке разработки -Use Bun to manage packages. +### Как начать -``` -bun i -``` +Используйте `bun i`, `npm i`, `pnpm i` или `yarn`, чтобы установить пакеты. Затем сгенерируйте сертификат и файл ключа для devserver с помощью `mkcert localhost`. + +### Настройка переменных + +- Используйте файл `.env` для настройки переменных собственной среды разработки. +- Переменные окружения с префиксом `PUBLIC_` широко используются в `/src/utils/config.ts`. + +### Полезные команды + +Запуск проверки соответствия типов и автоматически исправить ошибки стилей, порядок импорта, форматирование: -## Useful commands -run checks ``` bun run typecheck -``` -fix styles, imports, formatting and autofixable linting errors: -``` bun run fix ``` -## Config of variables -- All vars are already in place and wroted in - ``` - /src/utils/config.ts - ``` +## End-to-End (E2E) тесты -# 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` -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-with-auth`: Houses tests that interact with authenticated parts of the application. +- `tests`: Содержит тесты, которые не требуют аутентификации. +- `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` для упрощения процесса тестирования. diff --git a/app.config.ts b/app.config.ts index b2f15b04..c218dda3 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,4 +1,6 @@ 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 sassDts from 'vite-plugin-sass-dts' @@ -20,6 +22,7 @@ export default defineConfig({ vite: { envPrefix: 'PUBLIC_', plugins: [ + mkcert(), nodePolyfills({ include: ['path', 'stream', 'util'], exclude: ['http'], @@ -43,7 +46,10 @@ export default defineConfig({ }, build: { chunkSizeWarningLimit: 1024, - target: 'esnext' + target: 'esnext', + rollupOptions: { + plugins: [visualizer(), ] + } }, server: { https: true diff --git a/package.json b/package.json index c2a046c3..5e9f2661 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "prosemirror-history": "^1.4.1", "prosemirror-trailing-node": "^2.0.8", "prosemirror-view": "^1.33.8", + "rollup-plugin-visualizer": "^5.12.0", "sass": "1.76.0", "solid-js": "^1.8.18", "solid-popper": "^0.3.0", @@ -106,6 +107,7 @@ "typograf": "^7.4.1", "uniqolor": "^1.1.1", "vinxi": "^0.3.14", + "vite-plugin-mkcert": "^1.17.5", "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-sass-dts": "^1.3.24", "y-prosemirror": "1.2.9", diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 9e1bb61e..f8f42836 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -28,35 +28,42 @@ import styles from './Author.module.scss' type Props = { authorSlug: string + selectedTab: string shouts?: Shout[] author?: Author topics?: Topic[] - selectedTab: string } export const PRERENDERED_ARTICLES_COUNT = 12 const LOAD_MORE_PAGE_SIZE = 9 export const AuthorView = (props: Props) => { - console.debug('[components.AuthorView] reactive context init...') + // contexts const { t } = useLocalize() - const params = useParams() - const { followers: myFollowers, follows: myFollows } = useFollowing() - const { session } = useSession() - const me = createMemo(() => session()?.user?.app_data?.profile as Author) - const [authorSlug, setSlug] = createSignal(props.authorSlug) - const { sortedFeed } = useFeed() 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 [isBioExpanded, setIsBioExpanded] = createSignal(false) - const { loadAuthor, authorsEntities } = useAuthors() const [author, setAuthor] = createSignal() const [followers, setFollowers] = createSignal([] as Author[]) const [following, changeFollowing] = createSignal>([] as Array) // flat AuthorFollowsResult const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) const [commented, setCommented] = createSignal([]) - const { query } = useGraphQL() + // derivatives + const me = createMemo(() => session()?.user?.app_data?.profile as Author) + const pages = createMemo(() => + splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) + ) + + // fx // пагинация загрузки ленты постов const loadMore = async () => { saveScrollPosition() @@ -74,6 +81,7 @@ export const AuthorView = (props: Props) => { const [isFetching, setIsFetching] = createSignal(false) createEffect( on([() => session()?.user?.app_data?.profile, () => props.authorSlug || ''], async ([me, slug]) => { + console.debug('check if my profile') const my = slug && me?.slug === slug if (my) { console.debug('[Author] my profile precached') @@ -84,29 +92,27 @@ export const AuthorView = (props: Props) => { } } else if (slug && !isFetching()) { setIsFetching(true) - setSlug(slug) await loadAuthor({ slug }) setIsFetching(false) // Сброс состояния загрузки после завершения } - }) + }, {defer: true}) ) // 2 // догружает подписки автора createEffect( on( - [followers, () => props.author || authorsEntities()[authorSlug()]], - async ([current, found]) => { - if (current) return + () => authorsEntities()[props.author?.slug || props.authorSlug || ''], + async (found) => { if (!found) return setAuthor(found) - console.info(`[Author] profile for @${authorSlug()} fetched`) - const followsResp = await query(getAuthorFollowsQuery, { slug: authorSlug() }).toPromise() + console.info(`[Author] profile for @${found.slug} fetched`) + const followsResp = await query(getAuthorFollowsQuery, { slug: found.slug }).toPromise() const follows = followsResp?.data?.get_author_followers || {} changeFollowing([...(follows?.authors || []), ...(follows?.topics || [])]) - console.info(`[Author] follows for @${authorSlug()} fetched`) - const followersResp = await query(getAuthorFollowersQuery, { slug: authorSlug() }).toPromise() + console.info(`[Author] follows for @${found.slug} fetched`) + const followersResp = await query(getAuthorFollowersQuery, { slug: found.slug }).toPromise() setFollowers(followersResp?.data?.get_author_followers || []) - console.info(`[Author] followers for @${authorSlug()} fetched`) + console.info(`[Author] followers for @${found.slug} fetched`) setIsFetching(false) }, { defer: true } @@ -120,33 +126,32 @@ export const AuthorView = (props: Props) => { async (profile: Author) => { if (!commented() && profile) { await loadMore() - const commentsFetcher = loadReactions({ by: { comment: true, created_by: profile.id } }) const ccc = await commentsFetcher() if (ccc) setCommented((_) => ccc || []) } - } + }, // { defer: true }, ) ) + // event handlers let bioContainerRef: HTMLDivElement let bioWrapperRef: HTMLDivElement const checkBioHeight = () => { + console.debug('[AuthorView] mounted, checking bio height...') if (bioContainerRef) { setShowExpandBioControl(bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight) } } - const pages = createMemo(() => - splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) - ) const handleDeleteComment = (id: number) => { setCommented((prev) => (prev || []).filter((comment) => comment.id !== id)) } + // on load onMount(checkBioHeight) return ( diff --git a/src/context/authors.tsx b/src/context/authors.tsx index d8bd9209..c69475a8 100644 --- a/src/context/authors.tsx +++ b/src/context/authors.tsx @@ -95,6 +95,7 @@ export const AuthorsProvider = (props: { children: JSX.Element }) => { const fetcher = await getAuthor(opts) const author = await fetcher() if (author) addAuthor(author as Author) + console.debug('Loaded author:', author) } catch (error) { console.error('Error loading author:', error) throw error diff --git a/src/graphql/api/public.ts b/src/graphql/api/public.ts index 4a482794..3d00724a 100644 --- a/src/graphql/api/public.ts +++ b/src/graphql/api/public.ts @@ -106,7 +106,7 @@ export const loadFollowersByTopic = (slug: string) => { // TODO: paginate topic followers return cache(async () => { 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[] }, `topic-${slug}`) } diff --git a/src/routes/author/[slug]/(profile).tsx b/src/routes/author/[slug]/[...tab].tsx similarity index 93% rename from src/routes/author/[slug]/(profile).tsx rename to src/routes/author/[slug]/[...tab].tsx index 6c49969a..2ac239fb 100644 --- a/src/routes/author/[slug]/(profile).tsx +++ b/src/routes/author/[slug]/[...tab].tsx @@ -30,7 +30,7 @@ const fetchAllTopics = async () => { } 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() return aaa?.[0] } @@ -78,6 +78,8 @@ export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; to ? getImageUrl(author()?.pic || '', { width: 1200 }) : getImageUrl('production/image/logo_image.png') ) + + const selectedTab = createMemo(() => params.tab in ['followers', 'shouts'] ? params.tab : 'name') return ( }> }> @@ -91,9 +93,9 @@ export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; to diff --git a/src/routes/author/[slug]/[tab].tsx b/src/routes/author/[slug]/[tab].tsx deleted file mode 100644 index 5aab27e3..00000000 --- a/src/routes/author/[slug]/[tab].tsx +++ /dev/null @@ -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 ( - }> - }> - - - - - - - - ) -}