diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 000000000000..78fdeb313ed3 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,23 @@ +import {Contributor} from './types'; + +export class ClientApi { + private static _instance: ClientApi; + + static get instance(): ClientApi { + if (!ClientApi._instance) { + ClientApi._instance = new ClientApi(); + } + + return ClientApi._instance; + } + + async fetchAllContributors(): Promise { + const res = await fetch('api/contributors'); + + if (!res.ok) { + return []; + } + + return (await res.json())?.contributors || []; + } +} diff --git a/src/api/index.ts b/src/api/index.ts index fa6f4d45a360..6990100c75d9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,591 +1,14 @@ -import fs from 'fs'; -import path from 'path'; -import {fileURLToPath} from 'url'; - -import {createAppAuth} from '@octokit/auth-app'; -import {Octokit} from '@octokit/rest'; -import {i18n} from 'next-i18next.config'; - -import {type LibConfig, libs as libsConfigs} from '../libs'; - -export type Contributor = { - login: string; - url: string; - avatarUrl: string; - contributions: number; -}; - -export type CodeOwners = { - pattern: string; - owners: string[]; -}; - -export type LibMetadata = { - stars: number; - version: string; - lastUpdate: string; - license: string; - issues: number; -}; - -export type LibData = { - readme: { - en: string; - ru: string; - es: string; - zh: string; - fr: string; - de: string; - ko: string; - }; - changelog: string; - contributors: Contributor[]; - codeOwners: CodeOwners[]; -}; - -export type LibBase = { - config: LibConfig; -}; - -export type LibWithMetadata = LibBase & { - metadata: LibMetadata; -}; - -export type LibWithFullData = LibBase & LibWithMetadata & {data: LibData}; - -export type NpmInfo = { - 'dist-tags'?: { - latest?: string; - }; - time?: { - [version: string]: string; - }; -}; - -export type GithubInfo = { - stargazers_count?: number; - license?: { - name?: string; - } | null; - open_issues_count?: number; - contributors: Contributor[]; - codeOwners: CodeOwners[]; -}; - -export class Api { - private static _instance: Api; - - private octokit: Octokit; - - private readonly CONTRIBUTORS_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours - private contributorsCache: {data: Contributor[]; timestamp: number} | null = null; - - private readonly LIBS_CACHE_TTL = 60 * 60 * 1000; // 1 hour - private libsByIdCache: Record = {}; - - private readonly COMPONENT_README_CACHE_TTL = 60 * 60 * 1000; // 1 hour - private componentReadmeCache: Record = {}; - - private readonly CONTRIBUTOR_IGNORE_LIST = [ - 'dependabot', - 'dependabot[bot]', - 'gravity-ui-bot', - 'yc-ui-bot', - ]; - - static get instance(): Api { - if (!Api._instance) { - Api._instance = new Api(); - } - - return Api._instance; - } - - constructor() { - const octokitParams = process.env.GITHUB_APP_ID - ? { - authStrategy: createAppAuth, - auth: { - appId: process.env.GITHUB_APP_ID, - installationId: process.env.GITHUB_APP_INSTALLATION_ID, - privateKey: process.env.GITHUB_APP_PRIVATE_KEY, - }, - } - : {auth: process.env.GITHUB_TOKEN}; - - this.octokit = new Octokit(octokitParams); - } - - async getRepositoryContributors(repoOwner: string, repo: string): Promise { - const items = await this.octokit.paginate(this.octokit.rest.repos.listContributors, { - owner: repoOwner, - repo, - }); - - const contributors = items - .filter(({login}) => login && !this.CONTRIBUTOR_IGNORE_LIST.includes(login)) - .map(({login, avatar_url: avatarUrl, html_url: url, contributions}) => ({ - login: login!, - avatarUrl: avatarUrl!, - url: url!, - contributions, - })); - - return contributors; - } - - async fetchRepositoryCodeOwners(repoOwner: string, repo: string): Promise { - const url = `https://raw.githubusercontent.com/${repoOwner}/${repo}/main/CODEOWNERS`; - const res = await fetch(url); - - if (!res.ok) { - return []; - } - - const codeOwnersText = await res.text(); - const lines = codeOwnersText.split('\n'); - const codeOwners: CodeOwners[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - - if (!trimmed) { - continue; - } - - if (trimmed[0] === '#') { - continue; - } - - const [pattern, ...owners] = trimmed.split(' ').filter(Boolean); - const normalizedOwners = owners - .filter((owner) => owner.length > 1 && owner[0] === '@') - .map((owner) => owner.slice(1)); - - if (normalizedOwners.length) { - codeOwners.push({ - pattern, - owners: normalizedOwners, - }); - } - } - - return codeOwners; - } - - async getOrganizationRepositories(org: string) { - return await this.octokit.paginate(this.octokit.rest.repos.listForOrg, { - org, - headers: { - 'X-GitHub-Api-Version': '2022-11-28', - }, - }); - } - - async fetchAllContributors(): Promise { - try { - const repos = await this.getOrganizationRepositories('gravity-ui'); - - const rawContributors = await Promise.all( - repos.map(async (repo) => - this.getRepositoryContributors(repo.owner.login, repo.name), - ), - ); - - const contributors: Record = {}; - - for (const list of rawContributors) { - for (const contributor of list) { - const {login, contributions} = contributor; - - if (contributors[login]) { - contributors[login].contributions += contributions; - } else { - contributors[login] = contributor; - } - } - } - - const sortedContributors = Object.values(contributors).sort( - (a, b) => b.contributions - a.contributions, - ); - - return sortedContributors; - } catch (error) { - console.error('Error fetching contributors:', error); - return []; - } - } - - async fetchAllContributorsWithCache(): Promise { - const now = Date.now(); - - if (this.contributorsCache) { - const isCacheOutdated = - now - this.contributorsCache.timestamp > this.CONTRIBUTORS_CACHE_TTL; - - if (isCacheOutdated) { - this.fetchAllContributors() - .then((contributors) => { - this.contributorsCache = { - data: contributors, - timestamp: Date.now(), - }; - }) - .catch((error) => { - console.error('Error updating contributors cache:', error); - }); - } - - return this.contributorsCache.data; - } - - const contributors = await this.fetchAllContributors(); - - this.contributorsCache = { - data: contributors, - timestamp: now, - }; - - return contributors; - } - - async fetchNpmInfo(npmId: string): Promise { - try { - const npmApiUrl = 'https://registry.npmjs.org/'; - const response = await fetch(`${npmApiUrl}${npmId}`); - - if (response.ok) { - return (await response.json()) as NpmInfo; - } - } catch (err) { - console.error(err); - } - - return null; - } - - async fetchLibGithubInfo(githubId: string): Promise { - try { - const [repoOwner, repo] = githubId.split('/'); - - const [repoData, contributors, codeOwners] = await Promise.all([ - this.octokit.rest.repos - .get({ - owner: repoOwner, - repo, - }) - .then((response) => response.data), - this.getRepositoryContributors(repoOwner, repo), - this.fetchRepositoryCodeOwners(repoOwner, repo), - ]); - - const result: GithubInfo = { - ...repoData, - contributors, - codeOwners, - }; - - return result; - } catch (err) { - console.error(err); - } - - return null; - } - - async fetchChangelogInfo(changelogUrl: string): Promise { - if (!changelogUrl) return ''; - - const headers: Record = {'User-Agent': 'request'}; - - try { - const response = await fetch(changelogUrl, { - headers, - }); - if (response.ok) { - return await response.text(); - } - } catch (err) { - console.error(err); - } - - return ''; - } - - async fetchLibReadmeInfo({readmeUrl, id}: LibConfig): Promise<{ - en: string; - ru: string; - es: string; - zh: string; - fr: string; - de: string; - ko: string; - }> { - const headers: Record = {'User-Agent': 'request'}; - - const fetchReadmeContent = async (url: string) => { - try { - const response = await fetch(url, { - headers, - }); - if (response.ok) { - return await response.text(); - } - } catch (err) { - console.error(err); - } - - return ''; - }; - - const getLocalDocsReadPromise = (locale: string) => - fs.promises.readFile( - path.join( - path.dirname(fileURLToPath(import.meta.url)), - `../../src/content/local-docs/libs/${id}/README-${locale}.md`, - ), - 'utf8', - ); - - const [en, ru, es, zh, fr, de, ko] = await Promise.all([ - fetchReadmeContent(readmeUrl.en), - fetchReadmeContent(readmeUrl.ru), - getLocalDocsReadPromise('es'), - getLocalDocsReadPromise('zh'), - getLocalDocsReadPromise('fr'), - getLocalDocsReadPromise('de'), - getLocalDocsReadPromise('ko'), - ]); - - return { - en, - ru, - es, - zh, - fr, - de, - ko, - }; - } - - async fetchLibData(libConfig: LibConfig): Promise> { - const [npmInfo, githubInfo, readmeInfo, changelogInfo] = await Promise.all([ - libConfig.npmId ? this.fetchNpmInfo(libConfig.npmId) : null, - libConfig.githubId ? this.fetchLibGithubInfo(libConfig.githubId) : null, - this.fetchLibReadmeInfo(libConfig), - libConfig.changelogUrl ? this.fetchChangelogInfo(libConfig.changelogUrl) : '', - ]); - - const latestVersion = npmInfo?.['dist-tags']?.latest || ''; - let latestReleaseDate = ''; - - if (latestVersion && npmInfo?.time?.[latestVersion]) { - try { - const date = new Date(npmInfo.time[latestVersion]); - const day = date.getUTCDate(); - const month = date.getUTCMonth() + 1; - latestReleaseDate = `${day < 10 ? `0${day}` : day}.${ - month < 10 ? `0${month}` : month - }.${date.getUTCFullYear()}`; - } catch (error) { - console.error('Error parsing date:', error); - } - } - - return { - metadata: { - stars: githubInfo?.stargazers_count ?? 0, - version: latestVersion, - lastUpdate: latestReleaseDate, - license: githubInfo?.license?.name ?? '', - issues: githubInfo?.open_issues_count ?? 0, - }, - data: { - readme: readmeInfo, - changelog: changelogInfo, - contributors: githubInfo?.contributors ?? [], - codeOwners: githubInfo?.codeOwners ?? [], - }, - }; - } - - async fetchLibById(id: string): Promise { - const config = libsConfigs.find((lib) => lib.id === id); - - if (!config) { - throw new Error(`Can't find config for lib with id – ${id}`); - } - - const {metadata, data} = await this.fetchLibData(config); - - return { - config, - metadata, - data, - }; - } - - async fetchLibByIdWithCache(id: string): Promise { - const now = Date.now(); - - if (this.libsByIdCache[id]) { - const isCacheOutdated = now - this.libsByIdCache[id].timestamp > this.LIBS_CACHE_TTL; - - if (isCacheOutdated) { - this.fetchLibById(id) - .then((lib) => { - this.libsByIdCache[id] = { - timestamp: Date.now(), - lib, - }; - }) - .catch((error) => { - console.error(`Error updating lib cache for ${id}:`, error); - }); - } - - return this.libsByIdCache[id].lib; - } - - const lib = await this.fetchLibById(id); - this.libsByIdCache[id] = { - timestamp: now, - lib, - }; - - return lib; - } - - fetchAllLibs(): Promise { - return Promise.all(libsConfigs.map(({id}) => this.fetchLibByIdWithCache(id))); - } - - fetchAllLibsOnlyWithMetadata(): Promise { - return Promise.all( - libsConfigs.map(({id}) => - this.fetchLibByIdWithCache(id).then(({config, metadata}) => ({ - config, - metadata, - })), - ), - ); - } - - async fetchLandingLibs(): Promise { - const libs = await Promise.all( - libsConfigs - .filter(({landing}) => landing) - .map(({id}) => - this.fetchLibByIdWithCache(id).then(({config, metadata}) => ({ - config, - metadata, - })), - ), - ); - - return libs.sort((lib1, lib2) => { - const order = [ - 'uikit', - 'navigation', - 'date-components', - 'markdown-editor', - 'graph', - 'page-constructor', - 'dashkit', - ]; - - return order.indexOf(lib1.config.id) - order.indexOf(lib2.config.id); - }); - } - - async fetchComponentReadme({ - readmeUrl, - libId, - componentId, - locale, - }: { - readmeUrl: Record; - libId: string; - componentId: string; - locale: string; - }): Promise { - let readmeContent = ''; - - const headers: Record = {'User-Agent': 'request'}; - - try { - if (locale !== 'en' && locale !== 'ru') { - try { - readmeContent = await import( - `../content/local-docs/components/${libId}/${componentId}/README-${locale}.md` - ).then((module) => module.default); - } catch (err) { - console.warn( - `Can't find local docs for "${componentId}", library "${libId}", lang "${locale}"`, - ); - } - } else { - const res = await fetch(readmeUrl[locale], {headers}); - if (res.status >= 200 && res.status < 300) { - readmeContent = await res.text(); - } - } - - if (!readmeContent && locale !== i18n.defaultLocale) { - const fallbackRes = await fetch(readmeUrl[i18n.defaultLocale], {headers}); - if (fallbackRes.status >= 200 && fallbackRes.status < 300) { - readmeContent = await fallbackRes.text(); - } - } - } catch (err) { - console.warn('Error fetching component README:', err); - } - - return readmeContent; - } - - async fetchComponentReadmeWithCache({ - readmeUrl, - libId, - componentId, - locale, - }: { - readmeUrl: Record; - libId: string; - componentId: string; - locale: string; - }): Promise { - const cacheKey = `${libId}:${componentId}:${locale}`; - const now = Date.now(); - - if (this.componentReadmeCache[cacheKey]) { - const isCacheOutdated = - now - this.componentReadmeCache[cacheKey].timestamp > - this.COMPONENT_README_CACHE_TTL; - - if (isCacheOutdated) { - this.fetchComponentReadme({readmeUrl, libId, componentId, locale}) - .then((content) => { - this.componentReadmeCache[cacheKey] = { - content, - timestamp: Date.now(), - }; - }) - .catch((error) => { - console.error(`Error updating README cache for ${cacheKey}:`, error); - }); - } - - return this.componentReadmeCache[cacheKey].content; - } - - const content = await this.fetchComponentReadme({readmeUrl, libId, componentId, locale}); - this.componentReadmeCache[cacheKey] = { - content, - timestamp: now, - }; - - return content; - } -} +export type { + Contributor, + CodeOwners, + LibMetadata, + LibData, + LibBase, + LibWithMetadata, + LibWithFullData, + NpmInfo, + GithubInfo, +} from './types'; + +export {ServerApi} from './server'; +export {ClientApi} from './client'; diff --git a/src/api/server.ts b/src/api/server.ts new file mode 100644 index 000000000000..3bbb9eca505a --- /dev/null +++ b/src/api/server.ts @@ -0,0 +1,528 @@ +import fs from 'fs'; +import path from 'path'; +import {fileURLToPath} from 'url'; + +import {createAppAuth} from '@octokit/auth-app'; +import {Octokit} from '@octokit/rest'; +import {i18n} from 'next-i18next.config'; + +import {type LibConfig, libs as libsConfigs} from '../libs'; + +import type { + CodeOwners, + Contributor, + GithubInfo, + LibWithFullData, + LibWithMetadata, + NpmInfo, +} from './types'; + +export class ServerApi { + private static _instance: ServerApi; + + private octokit: Octokit; + + private readonly CONTRIBUTORS_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours + private contributorsCache: {data: Contributor[]; timestamp: number} | null = null; + + private readonly LIBS_CACHE_TTL = 60 * 60 * 1000; // 1 hour + private libsByIdCache: Record = {}; + + private readonly COMPONENT_README_CACHE_TTL = 60 * 60 * 1000; // 1 hour + private componentReadmeCache: Record = {}; + + private readonly CONTRIBUTOR_IGNORE_LIST = [ + 'dependabot', + 'dependabot[bot]', + 'gravity-ui-bot', + 'yc-ui-bot', + ]; + + static get instance(): ServerApi { + if (!ServerApi._instance) { + ServerApi._instance = new ServerApi(); + } + + return ServerApi._instance; + } + + constructor() { + const octokitParams = process.env.GITHUB_APP_ID + ? { + authStrategy: createAppAuth, + auth: { + appId: process.env.GITHUB_APP_ID, + installationId: process.env.GITHUB_APP_INSTALLATION_ID, + privateKey: process.env.GITHUB_APP_PRIVATE_KEY, + }, + } + : {auth: process.env.GITHUB_TOKEN}; + + this.octokit = new Octokit(octokitParams); + } + + async getRepositoryContributors(repoOwner: string, repo: string): Promise { + const items = await this.octokit.paginate(this.octokit.rest.repos.listContributors, { + owner: repoOwner, + repo, + }); + + const contributors = items + .filter(({login}) => login && !this.CONTRIBUTOR_IGNORE_LIST.includes(login)) + .map(({login, avatar_url: avatarUrl, html_url: url, contributions}) => ({ + login: login!, + avatarUrl: avatarUrl!, + url: url!, + contributions, + })); + + return contributors; + } + + async fetchRepositoryCodeOwners(repoOwner: string, repo: string): Promise { + const url = `https://raw.githubusercontent.com/${repoOwner}/${repo}/main/CODEOWNERS`; + const res = await fetch(url); + + if (!res.ok) { + return []; + } + + const codeOwnersText = await res.text(); + const lines = codeOwnersText.split('\n'); + const codeOwners: CodeOwners[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + + if (!trimmed) { + continue; + } + + if (trimmed[0] === '#') { + continue; + } + + const [pattern, ...owners] = trimmed.split(' ').filter(Boolean); + const normalizedOwners = owners + .filter((owner) => owner.length > 1 && owner[0] === '@') + .map((owner) => owner.slice(1)); + + if (normalizedOwners.length) { + codeOwners.push({ + pattern, + owners: normalizedOwners, + }); + } + } + + return codeOwners; + } + + async getOrganizationRepositories(org: string) { + return await this.octokit.paginate(this.octokit.rest.repos.listForOrg, { + org, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + } + + async fetchAllContributors(): Promise { + try { + const repos = await this.getOrganizationRepositories('gravity-ui'); + + const rawContributors = await Promise.all( + repos.map(async (repo) => + this.getRepositoryContributors(repo.owner.login, repo.name), + ), + ); + + const contributors: Record = {}; + + for (const list of rawContributors) { + for (const contributor of list) { + const {login, contributions} = contributor; + + if (contributors[login]) { + contributors[login].contributions += contributions; + } else { + contributors[login] = contributor; + } + } + } + + const sortedContributors = Object.values(contributors).sort( + (a, b) => b.contributions - a.contributions, + ); + + return sortedContributors; + } catch (error) { + console.error('Error fetching contributors:', error); + return []; + } + } + + async fetchAllContributorsWithCache(): Promise { + console.log('--------------------------------------------------------------'); + console.log('contributors:', this.contributorsCache); + console.log('--------------------------------------------------------------'); + const now = Date.now(); + + if (this.contributorsCache) { + const isCacheOutdated = + now - this.contributorsCache.timestamp > this.CONTRIBUTORS_CACHE_TTL; + + if (isCacheOutdated) { + this.fetchAllContributors() + .then((contributors) => { + this.contributorsCache = { + data: contributors, + timestamp: Date.now(), + }; + }) + .catch((error) => { + console.error('Error updating contributors cache:', error); + }); + } + + return this.contributorsCache.data; + } + + const contributors = await this.fetchAllContributors(); + + this.contributorsCache = { + data: contributors, + timestamp: now, + }; + + return contributors; + } + + async fetchNpmInfo(npmId: string): Promise { + try { + const npmApiUrl = 'https://registry.npmjs.org/'; + const response = await fetch(`${npmApiUrl}${npmId}`); + + if (response.ok) { + return (await response.json()) as NpmInfo; + } + } catch (err) { + console.error(err); + } + + return null; + } + + async fetchLibGithubInfo(githubId: string): Promise { + try { + const [repoOwner, repo] = githubId.split('/'); + + const [repoData, contributors, codeOwners] = await Promise.all([ + this.octokit.rest.repos + .get({ + owner: repoOwner, + repo, + }) + .then((response) => response.data), + this.getRepositoryContributors(repoOwner, repo), + this.fetchRepositoryCodeOwners(repoOwner, repo), + ]); + + const result: GithubInfo = { + ...repoData, + contributors, + codeOwners, + }; + + return result; + } catch (err) { + console.error(err); + } + + return null; + } + + async fetchChangelogInfo(changelogUrl: string): Promise { + if (!changelogUrl) return ''; + + const headers: Record = {'User-Agent': 'request'}; + + try { + const response = await fetch(changelogUrl, { + headers, + }); + if (response.ok) { + return await response.text(); + } + } catch (err) { + console.error(err); + } + + return ''; + } + + async fetchLibReadmeInfo({ + readmeUrl, + id, + }: LibConfig): Promise<{en: string; ru: string; es: string; zh: string}> { + const headers: Record = {'User-Agent': 'request'}; + + const fetchReadmeContent = async (url: string) => { + try { + const response = await fetch(url, { + headers, + }); + if (response.ok) { + return await response.text(); + } + } catch (err) { + console.error(err); + } + + return ''; + }; + + const getLocalDocsReadPromise = (locale: string) => + fs.promises.readFile( + path.join( + path.dirname(fileURLToPath(import.meta.url)), + `../../src/content/local-docs/libs/${id}/README-${locale}.md`, + ), + 'utf8', + ); + + const [en, ru, es, zh] = await Promise.all([ + fetchReadmeContent(readmeUrl.en), + fetchReadmeContent(readmeUrl.ru), + getLocalDocsReadPromise('es'), + getLocalDocsReadPromise('zh'), + ]); + + return { + en, + ru, + es, + zh, + }; + } + + async fetchLibData(libConfig: LibConfig): Promise> { + const [npmInfo, githubInfo, readmeInfo, changelogInfo] = await Promise.all([ + libConfig.npmId ? this.fetchNpmInfo(libConfig.npmId) : null, + libConfig.githubId ? this.fetchLibGithubInfo(libConfig.githubId) : null, + this.fetchLibReadmeInfo(libConfig), + libConfig.changelogUrl ? this.fetchChangelogInfo(libConfig.changelogUrl) : '', + ]); + + const latestVersion = npmInfo?.['dist-tags']?.latest || ''; + let latestReleaseDate = ''; + + if (latestVersion && npmInfo?.time?.[latestVersion]) { + try { + const date = new Date(npmInfo.time[latestVersion]); + const day = date.getUTCDate(); + const month = date.getUTCMonth() + 1; + latestReleaseDate = `${day < 10 ? `0${day}` : day}.${ + month < 10 ? `0${month}` : month + }.${date.getUTCFullYear()}`; + } catch (error) { + console.error('Error parsing date:', error); + } + } + + return { + metadata: { + stars: githubInfo?.stargazers_count ?? 0, + version: latestVersion, + lastUpdate: latestReleaseDate, + license: githubInfo?.license?.name ?? '', + issues: githubInfo?.open_issues_count ?? 0, + }, + data: { + readme: readmeInfo, + changelog: changelogInfo, + contributors: githubInfo?.contributors ?? [], + codeOwners: githubInfo?.codeOwners ?? [], + }, + }; + } + + async fetchLibById(id: string): Promise { + const config = libsConfigs.find((lib) => lib.id === id); + + if (!config) { + throw new Error(`Can't find config for lib with id – ${id}`); + } + + const {metadata, data} = await this.fetchLibData(config); + + return { + config, + metadata, + data, + }; + } + + async fetchLibByIdWithCache(id: string): Promise { + const now = Date.now(); + + if (this.libsByIdCache[id]) { + const isCacheOutdated = now - this.libsByIdCache[id].timestamp > this.LIBS_CACHE_TTL; + + if (isCacheOutdated) { + this.fetchLibById(id) + .then((lib) => { + this.libsByIdCache[id] = { + timestamp: Date.now(), + lib, + }; + }) + .catch((error) => { + console.error(`Error updating lib cache for ${id}:`, error); + }); + } + + return this.libsByIdCache[id].lib; + } + + const lib = await this.fetchLibById(id); + this.libsByIdCache[id] = { + timestamp: now, + lib, + }; + + return lib; + } + + fetchAllLibs(): Promise { + return Promise.all(libsConfigs.map(({id}) => this.fetchLibByIdWithCache(id))); + } + + fetchAllLibsOnlyWithMetadata(): Promise { + return Promise.all( + libsConfigs.map(({id}) => + this.fetchLibByIdWithCache(id).then(({config, metadata}) => ({ + config, + metadata, + })), + ), + ); + } + + async fetchLandingLibs(): Promise { + const libs = await Promise.all( + libsConfigs + .filter(({landing}) => landing) + .map(({id}) => + this.fetchLibByIdWithCache(id).then(({config, metadata}) => ({ + config, + metadata, + })), + ), + ); + + return libs.sort((lib1, lib2) => { + const order = [ + 'uikit', + 'navigation', + 'date-components', + 'markdown-editor', + 'graph', + 'page-constructor', + 'dashkit', + ]; + + return order.indexOf(lib1.config.id) - order.indexOf(lib2.config.id); + }); + } + + async fetchComponentReadme({ + readmeUrl, + libId, + componentId, + locale, + }: { + readmeUrl: Record; + libId: string; + componentId: string; + locale: string; + }): Promise { + let readmeContent = ''; + + const headers: Record = {'User-Agent': 'request'}; + + try { + if (locale !== 'en' && locale !== 'ru') { + try { + readmeContent = await import( + `../content/local-docs/components/${libId}/${componentId}/README-${locale}.md` + ).then((module) => module.default); + } catch (err) { + console.warn( + `Can't find local docs for "${componentId}", library "${libId}", lang "${locale}"`, + ); + } + } else { + const res = await fetch(readmeUrl[locale], {headers}); + if (res.status >= 200 && res.status < 300) { + readmeContent = await res.text(); + } + } + + if (!readmeContent && locale !== i18n.defaultLocale) { + const fallbackRes = await fetch(readmeUrl[i18n.defaultLocale], {headers}); + if (fallbackRes.status >= 200 && fallbackRes.status < 300) { + readmeContent = await fallbackRes.text(); + } + } + } catch (err) { + console.warn('Error fetching component README:', err); + } + + return readmeContent; + } + + async fetchComponentReadmeWithCache({ + readmeUrl, + libId, + componentId, + locale, + }: { + readmeUrl: Record; + libId: string; + componentId: string; + locale: string; + }): Promise { + const cacheKey = `${libId}:${componentId}:${locale}`; + const now = Date.now(); + + if (this.componentReadmeCache[cacheKey]) { + const isCacheOutdated = + now - this.componentReadmeCache[cacheKey].timestamp > + this.COMPONENT_README_CACHE_TTL; + + if (isCacheOutdated) { + this.fetchComponentReadme({readmeUrl, libId, componentId, locale}) + .then((content) => { + this.componentReadmeCache[cacheKey] = { + content, + timestamp: Date.now(), + }; + }) + .catch((error) => { + console.error(`Error updating README cache for ${cacheKey}:`, error); + }); + } + + return this.componentReadmeCache[cacheKey].content; + } + + const content = await this.fetchComponentReadme({readmeUrl, libId, componentId, locale}); + this.componentReadmeCache[cacheKey] = { + content, + timestamp: now, + }; + + return content; + } +} diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 000000000000..cf62a652f838 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,62 @@ +import type {LibConfig} from '../libs'; + +export type Contributor = { + login: string; + url: string; + avatarUrl: string; + contributions: number; +}; + +export type CodeOwners = { + pattern: string; + owners: string[]; +}; + +export type LibMetadata = { + stars: number; + version: string; + lastUpdate: string; + license: string; + issues: number; +}; + +export type LibData = { + readme: { + en: string; + ru: string; + es: string; + zh: string; + }; + changelog: string; + contributors: Contributor[]; + codeOwners: CodeOwners[]; +}; + +export type LibBase = { + config: LibConfig; +}; + +export type LibWithMetadata = LibBase & { + metadata: LibMetadata; +}; + +export type LibWithFullData = LibBase & LibWithMetadata & {data: LibData}; + +export type NpmInfo = { + 'dist-tags'?: { + latest?: string; + }; + time?: { + [version: string]: string; + }; +}; + +export type GithubInfo = { + stargazers_count?: number; + license?: { + name?: string; + } | null; + open_issues_count?: number; + contributors: Contributor[]; + codeOwners: CodeOwners[]; +}; diff --git a/src/blocks/Contributors/Contributors.tsx b/src/blocks/Contributors/Contributors.tsx index 4ad66b6c122b..cba6ea28d3ff 100644 --- a/src/blocks/Contributors/Contributors.tsx +++ b/src/blocks/Contributors/Contributors.tsx @@ -1,44 +1,25 @@ -import {Animatable, AnimateBlock, HTML} from '@gravity-ui/page-constructor'; +import {AnimateBlock, HTML} from '@gravity-ui/page-constructor'; import {Button} from '@gravity-ui/uikit'; import React from 'react'; +import {LazyExpandableContributorsList} from 'src/components/ExpandableContributorList'; -import {Contributor} from '../../api'; -import {ExpandableContributorList} from '../../components/ExpandableContributorList'; import {block} from '../../utils'; -import {CustomBlock} from '../constants'; import './Contributors.scss'; +import {ContributorsProps} from './types'; const b = block('contributors'); -type TelegramLink = { - title: string; - href: string; -}; - -export type ContributorsProps = Animatable & { - title: string; - link: TelegramLink; - contributors: Contributor[]; -}; - -export type ContributorsModel = ContributorsProps & { - type: CustomBlock.Contributors; -}; +export const ContributorsBlock: React.FC = ({animated, title, link}) => { + const [contributorsAmount, setContributorsAmount] = React.useState(''); -export const ContributorsBlock: React.FC = ({ - animated, - title, - link, - contributors, -}) => { return (

{title}

-
{contributors.length}
+
{contributorsAmount}
- + { + setContributorsAmount(String(props.contributors.length) || '0'); + }} + />
); diff --git a/src/blocks/Contributors/types.ts b/src/blocks/Contributors/types.ts new file mode 100644 index 000000000000..7e6a8c13b5da --- /dev/null +++ b/src/blocks/Contributors/types.ts @@ -0,0 +1,17 @@ +import {Animatable} from '@gravity-ui/page-constructor'; + +import {CustomBlock} from '../constants'; + +type TelegramLink = { + title: string; + href: string; +}; + +export type ContributorsProps = Animatable & { + title: string; + link: TelegramLink; +}; + +export type ContributorsModel = ContributorsProps & { + type: CustomBlock.Contributors; +}; diff --git a/src/blocks/UISamples/samples.ts b/src/blocks/UISamples/samples.ts index 81ce66123889..9f8c8cf8e7ca 100644 --- a/src/blocks/UISamples/samples.ts +++ b/src/blocks/UISamples/samples.ts @@ -1,13 +1,13 @@ import {useTranslation} from 'next-i18next'; import {useMemo} from 'react'; import { - ApartmentCardPreview, - DashboardPreview2, - KubernetesPreview, - MailPreview, - OsnPreview, - TablePreview, - TasksPreview, + LazyApartmentCardPreview, + LazyDashboardPreview2, + LazyKubernetesPreview, + LazyMailPreview, + LazyOsnPreview, + LazyTablePreview, + LazyTasksPreview, } from 'src/components/UISamples'; import dashboardImage from '../../assets/ui-samples/card-dashboard.jpg'; @@ -36,7 +36,7 @@ export const useSampleComponents = () => { { type: SampleComponent.Dashboard, imagePreviewSrc: dashboardImage.src, - Component: DashboardPreview2, + Component: LazyDashboardPreview2, title: t('ui_samples_dashboard_tab'), breadCrumbsItems: ['Dashboard'], blank: true, @@ -44,42 +44,42 @@ export const useSampleComponents = () => { { type: SampleComponent.HotelBooking, imagePreviewSrc: hotelBookingImage.src, - Component: ApartmentCardPreview, + Component: LazyApartmentCardPreview, title: t('ui_samples_apartment_tab'), blank: true, }, { type: SampleComponent.Listing, imagePreviewSrc: listingImage.src, - Component: TablePreview, + Component: LazyTablePreview, title: t('ui_samples_table_tab'), breadCrumbsItems: ['Table'], }, { type: SampleComponent.TaskTracker, imagePreviewSrc: taskTrackerImage.src, - Component: TasksPreview, + Component: LazyTasksPreview, title: t('ui_samples_task_tracker_tab'), blank: true, }, { type: SampleComponent.Kubernetes, imagePreviewSrc: kubernetesImage.src, - Component: KubernetesPreview, + Component: LazyKubernetesPreview, title: t('ui_samples_kubernetes_tab'), blank: true, }, { type: SampleComponent.Osn, imagePreviewSrc: osnImage.src, - Component: OsnPreview, + Component: LazyOsnPreview, title: t('ui_samples_osn_tab'), blank: true, }, { type: SampleComponent.Mail, imagePreviewSrc: mailImage.src, - Component: MailPreview, + Component: LazyMailPreview, title: t('ui_samples_mail_tab'), blank: true, }, diff --git a/src/blocks/types.ts b/src/blocks/types.ts index 9a92f02912d7..b8cb9b2213fe 100644 --- a/src/blocks/types.ts +++ b/src/blocks/types.ts @@ -1,4 +1,4 @@ -import {ContributorsModel} from './Contributors/Contributors'; +import {ContributorsModel} from './Contributors/types'; import {CustomHeaderModel} from './CustomHeader/CustomHeader'; import {ExamplesModel} from './Examples/Examples'; import {GithubStarsModel} from './GithubStarsBlock/GithubStarsBlock'; diff --git a/src/components/ExpandableContributorList/ExpandableContributorList.scss b/src/components/ExpandableContributorList/ExpandableContributorList.scss index 4a1c837f73ad..ea99a7e7fbbd 100644 --- a/src/components/ExpandableContributorList/ExpandableContributorList.scss +++ b/src/components/ExpandableContributorList/ExpandableContributorList.scss @@ -19,6 +19,14 @@ $block: '.#{variables.$ns}expandable-contributor-list'; grid-template-rows: unset; } + &__loader { + width: 100%; + height: 276px; + display: flex; + justify-content: center; + align-items: center; + } + &__inset-shadow { position: absolute; bottom: 0; diff --git a/src/components/ExpandableContributorList/LazyExpandableContributorsList.tsx b/src/components/ExpandableContributorList/LazyExpandableContributorsList.tsx new file mode 100644 index 000000000000..3004db6ce307 --- /dev/null +++ b/src/components/ExpandableContributorList/LazyExpandableContributorsList.tsx @@ -0,0 +1,38 @@ +import {Loader} from '@gravity-ui/uikit'; +import {ClientApi} from 'src/api/client'; +import {block} from 'src/utils'; + +import {IntersectionLoadComponent} from '../IntersectionLoadComponent/IntersectionLoadComponent'; + +type Props = Omit< + React.ComponentProps< + typeof IntersectionLoadComponent>> + >, + 'cacheKey' | 'getComponent' | 'getComponentProps' | 'loader' +>; + +import './ExpandableContributorList.scss'; + +const b = block('expandable-contributor-list'); + +const getComponent = async () => { + return (await import('./ExpandableContributorList')).ExpandableContributorList; +}; + +const getComponentProps = async () => { + const contributors = await ClientApi.instance.fetchAllContributors(); + + return {contributors}; +}; + +export const LazyExpandableContributorsList: React.FC = (props) => { + return ( + } + {...props} + /> + ); +}; diff --git a/src/components/ExpandableContributorList/index.ts b/src/components/ExpandableContributorList/index.ts index 1704cad4cefd..0c530c5b9983 100644 --- a/src/components/ExpandableContributorList/index.ts +++ b/src/components/ExpandableContributorList/index.ts @@ -1 +1,2 @@ export * from './ExpandableContributorList'; +export {LazyExpandableContributorsList} from './LazyExpandableContributorsList'; diff --git a/src/components/IntersectionLoadComponent/IntersectionLoadComponent.tsx b/src/components/IntersectionLoadComponent/IntersectionLoadComponent.tsx new file mode 100644 index 000000000000..818bc4890d49 --- /dev/null +++ b/src/components/IntersectionLoadComponent/IntersectionLoadComponent.tsx @@ -0,0 +1,94 @@ +'use client'; + +import {useIntersection} from '@gravity-ui/uikit'; +import React from 'react'; + +type Props = { + cacheKey: string; + getComponent: () => Promise; + getComponentProps?: NoInfer<() => Promise>>; + loader: React.ReactNode; + intersectionOptions?: IntersectionObserverInit; + onIntersect?: () => void; + onLoad?: NoInfer<(component: Component, props: React.ComponentProps) => void>; + wrapperClassName?: string; +}; + +const cache = new Map(); + +export const IntersectionLoadComponent = >({ + cacheKey, + getComponent, + getComponentProps = () => Promise.resolve({} as React.ComponentProps), + loader, + intersectionOptions, + onIntersect, + onLoad, + wrapperClassName, +}: Props) => { + const [waitingLoad, setWaitingLoad] = React.useState(!cache.has(cacheKey)); + const [intersectionElementRef, setIntersectionElementRef] = + React.useState(null); + + const getComponentWithPropsCached = React.useCallback(async () => { + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const [Component, props] = await Promise.all([getComponent(), getComponentProps()]); + + cache.set(cacheKey, {Component, props}); + + return {Component, props}; + }, [getComponent]); + + const LazyComponent = React.useMemo( + () => + React.lazy(async () => { + const {Component, props} = await getComponentWithPropsCached(); + + onLoad?.(Component, props); + + return {default: () => }; + }), + [getComponentWithPropsCached, onLoad], + ); + + useIntersection({ + element: intersectionElementRef, + onIntersect: () => { + setWaitingLoad(false); + onIntersect?.(); + }, + options: intersectionOptions, + }); + + if (cache.has(cacheKey)) { + const {Component, props} = cache.get(cacheKey); + + return ( +
+ +
+ ); + } + + if (waitingLoad) { + return ( +
+
+ {loader} +
+ ); + } + + return ( +
+ + + +
+ ); +}; + +IntersectionLoadComponent.clearCache = (key?: string) => (key ? cache.delete(key) : cache.clear()); diff --git a/src/components/Landing/Landing.tsx b/src/components/Landing/Landing.tsx index f7ea91b059ce..ddb3085ca63a 100644 --- a/src/components/Landing/Landing.tsx +++ b/src/components/Landing/Landing.tsx @@ -2,12 +2,12 @@ import {PageConstructor, PageContent} from '@gravity-ui/page-constructor'; import {useTranslation} from 'next-i18next'; import {useRouter} from 'next/router'; import React from 'react'; +import {ContributorsBlock} from 'src/blocks/Contributors/Contributors'; import Examples from 'src/blocks/Examples/Examples'; import {UISamplesBlock} from 'src/blocks/UISamples/UISamples'; import {CustomPageContent} from 'src/content/types'; -import type {Contributor, LibWithMetadata} from '../../api'; -import {ContributorsBlock} from '../../blocks/Contributors/Contributors'; +import type {LibWithMetadata} from '../../api'; import {CustomHeader} from '../../blocks/CustomHeader/CustomHeader'; import {GithubStarsBlock} from '../../blocks/GithubStarsBlock/GithubStarsBlock'; import {IFrameBlock} from '../../blocks/IFrameBlock/IFrameBlock'; @@ -41,11 +41,10 @@ const filterBlocks = ({blocks, ...rest}: CustomPageContent): CustomPageContent = type Props = { libs: LibWithMetadata[]; - contributors: Contributor[]; backgroundImageSrc: string; }; -export const Landing: React.FC = ({libs, contributors, backgroundImageSrc}) => { +export const Landing: React.FC = ({libs, backgroundImageSrc}) => { const {t} = useTranslation(); const {pathname} = useRouter(); @@ -58,8 +57,8 @@ export const Landing: React.FC = ({libs, contributors, backgroundImageSrc content={ filterBlocks( pathname === '/rtl' - ? getRtlLanding({t, libs, contributors, backgroundImageSrc}) - : getLanding({t, libs, contributors, backgroundImageSrc}), + ? getRtlLanding({t, libs, backgroundImageSrc}) + : getLanding({t, libs, backgroundImageSrc}), ) as PageContent } custom={{ diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index c03c458d0424..69ad52552054 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -72,7 +72,7 @@ export const Layout: React.FC = ({ }; }, [isRtl]); - const pageConent = ( + const pageContent = (
{!showOnlyContent && (
@@ -101,10 +101,10 @@ export const Layout: React.FC = ({ {isPageConstructor ? ( - {pageConent} + {pageContent} ) : ( - pageConent + pageContent )} diff --git a/src/components/UISamples/ApartmentCardPreview/LazyApartmentCardPreview.tsx b/src/components/UISamples/ApartmentCardPreview/LazyApartmentCardPreview.tsx new file mode 100644 index 000000000000..a34ed8e7f7ee --- /dev/null +++ b/src/components/UISamples/ApartmentCardPreview/LazyApartmentCardPreview.tsx @@ -0,0 +1,15 @@ +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; + +import {UISamplesLoader} from '../UISamplesLoader/UISamplesLoader'; + +const getComponent = async () => (await import('./ApartmentCardPreview')).ApartmentCardPreview; + +export const LazyApartmentCardPreview: React.FC = () => { + return ( + } + /> + ); +}; diff --git a/src/components/UISamples/DashboardPreview2/LazyDashboardPreview2.tsx b/src/components/UISamples/DashboardPreview2/LazyDashboardPreview2.tsx new file mode 100644 index 000000000000..e986ffd80e5a --- /dev/null +++ b/src/components/UISamples/DashboardPreview2/LazyDashboardPreview2.tsx @@ -0,0 +1,15 @@ +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; + +import {UISamplesLoader} from '../UISamplesLoader/UISamplesLoader'; + +const getComponent = async () => (await import('./DashboardPreview2')).DashboardPreview2; + +export const LazyDashboardPreview2: React.FC = () => { + return ( + } + /> + ); +}; diff --git a/src/components/UISamples/KubernetesPreview/LazyKubernetesPreview.tsx b/src/components/UISamples/KubernetesPreview/LazyKubernetesPreview.tsx new file mode 100644 index 000000000000..7b5e571001b0 --- /dev/null +++ b/src/components/UISamples/KubernetesPreview/LazyKubernetesPreview.tsx @@ -0,0 +1,15 @@ +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; + +import {UISamplesLoader} from '../UISamplesLoader/UISamplesLoader'; + +const getComponent = async () => (await import('./KubernetesPreview')).KubernetesPreview; + +export const LazyKubernetesPreview: React.FC = () => { + return ( + } + /> + ); +}; diff --git a/src/components/UISamples/MailPreview/LazyMailPreview.tsx b/src/components/UISamples/MailPreview/LazyMailPreview.tsx new file mode 100644 index 000000000000..de7838427ec3 --- /dev/null +++ b/src/components/UISamples/MailPreview/LazyMailPreview.tsx @@ -0,0 +1,15 @@ +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; + +import {UISamplesLoader} from '../UISamplesLoader/UISamplesLoader'; + +const getComponent = async () => (await import('./MailPreview')).MailPreview; + +export const LazyMailPreview: React.FC = () => { + return ( + } + /> + ); +}; diff --git a/src/components/UISamples/OsnPreview/LazyOsnPreview.tsx b/src/components/UISamples/OsnPreview/LazyOsnPreview.tsx new file mode 100644 index 000000000000..a97075e4f6ba --- /dev/null +++ b/src/components/UISamples/OsnPreview/LazyOsnPreview.tsx @@ -0,0 +1,15 @@ +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; + +import {UISamplesLoader} from '../UISamplesLoader/UISamplesLoader'; + +const getComponent = async () => (await import('./OsnPreview')).OsnPreview; + +export const LazyOsnPreview: React.FC = () => { + return ( + } + /> + ); +}; diff --git a/src/components/UISamples/TablePreview/LazyTablePreview.tsx b/src/components/UISamples/TablePreview/LazyTablePreview.tsx new file mode 100644 index 000000000000..42efff54d4ef --- /dev/null +++ b/src/components/UISamples/TablePreview/LazyTablePreview.tsx @@ -0,0 +1,20 @@ +import {Loader} from '@gravity-ui/uikit'; +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; +import {block} from 'src/utils'; + +import './TablePreview.scss'; + +const b = block('table-preview'); + +const getComponent = async () => (await import('./TablePreview')).TablePreview; + +export const LazyTablePreview: React.FC = () => { + return ( + } + wrapperClassName={b('loader')} + /> + ); +}; diff --git a/src/components/UISamples/TablePreview/TablePreview.scss b/src/components/UISamples/TablePreview/TablePreview.scss index d93ba84c402a..3092bd7e5c6d 100644 --- a/src/components/UISamples/TablePreview/TablePreview.scss +++ b/src/components/UISamples/TablePreview/TablePreview.scss @@ -16,4 +16,13 @@ $block: '.#{variables.$ns}table-preview'; overflow-x: auto; white-space: nowrap; } + + &__loader { + width: 100%; + height: 100%; + + display: flex; + justify-content: center; + align-items: center; + } } diff --git a/src/components/UISamples/TasksPreview/LazyTasksPreview.tsx b/src/components/UISamples/TasksPreview/LazyTasksPreview.tsx new file mode 100644 index 000000000000..fd37425b7c4c --- /dev/null +++ b/src/components/UISamples/TasksPreview/LazyTasksPreview.tsx @@ -0,0 +1,15 @@ +import {IntersectionLoadComponent} from 'src/components/IntersectionLoadComponent/IntersectionLoadComponent'; + +import {UISamplesLoader} from '../UISamplesLoader/UISamplesLoader'; + +const getComponent = async () => (await import('./TasksPreview')).TasksPreview; + +export const LazyTasksPreview: React.FC = () => { + return ( + } + /> + ); +}; diff --git a/src/components/UISamples/UISamplesLoader/UISamplesLoader.scss b/src/components/UISamples/UISamplesLoader/UISamplesLoader.scss new file mode 100644 index 000000000000..e0d1eb5eb6da --- /dev/null +++ b/src/components/UISamples/UISamplesLoader/UISamplesLoader.scss @@ -0,0 +1,16 @@ +@use '../../../variables'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}uisamples-loader'; + +#{$block} { + width: 100%; + height: 800px; + + display: flex; + justify-content: center; + align-items: center; + + border: 1px solid var(--g-color-line-generic); + border-radius: 16px; +} diff --git a/src/components/UISamples/UISamplesLoader/UISamplesLoader.tsx b/src/components/UISamples/UISamplesLoader/UISamplesLoader.tsx new file mode 100644 index 000000000000..2f2939a47f24 --- /dev/null +++ b/src/components/UISamples/UISamplesLoader/UISamplesLoader.tsx @@ -0,0 +1,10 @@ +import {Loader} from '@gravity-ui/uikit'; +import {block} from 'src/utils'; + +import './UISamplesLoader.scss'; + +const b = block('uisamples-loader'); + +export const UISamplesLoader: React.FC = () => { + return ; +}; diff --git a/src/components/UISamples/index.ts b/src/components/UISamples/index.ts index 4c38bcada0c6..a69c4b421706 100644 --- a/src/components/UISamples/index.ts +++ b/src/components/UISamples/index.ts @@ -1,13 +1,20 @@ export {ApartmentCardPreview} from './ApartmentCardPreview/ApartmentCardPreview'; +export {LazyApartmentCardPreview} from './ApartmentCardPreview/LazyApartmentCardPreview'; export {CardsPreview} from './CardsPreview/CardsPreview'; export {DashboardPreview2} from './DashboardPreview2/DashboardPreview2'; +export {LazyDashboardPreview2} from './DashboardPreview2/LazyDashboardPreview2'; export {DashboardPreview} from './DashboardsPreview/DashboardPreview'; export {FormPreview} from './FormPreview/FormPreview'; export {KubernetesPreview} from './KubernetesPreview/KubernetesPreview'; +export {LazyKubernetesPreview} from './KubernetesPreview/LazyKubernetesPreview'; export {MailPreview} from './MailPreview/MailPreview'; +export {LazyMailPreview} from './MailPreview/LazyMailPreview'; export {OsnPreview} from './OsnPreview/OsnPreview'; +export {LazyOsnPreview} from './OsnPreview/LazyOsnPreview'; export {PreviewLayout} from './PreviewLayout/PreviewLayout'; export {PreviewWrapper} from './PreviewWrapper/PreviewWrapper'; export {TablePreview} from './TablePreview/TablePreview'; +export {LazyTablePreview} from './TablePreview/LazyTablePreview'; export {TasksPreview} from './TasksPreview/TasksPreview'; +export {LazyTasksPreview} from './TasksPreview/LazyTasksPreview'; export * from './constants'; diff --git a/src/content/landing-rtl.ts b/src/content/landing-rtl.ts index 4744e3ed3475..8a4e6a30a0a9 100644 --- a/src/content/landing-rtl.ts +++ b/src/content/landing-rtl.ts @@ -1,7 +1,7 @@ import {BlockType} from '@gravity-ui/page-constructor'; import {TFunction} from 'next-i18next'; -import {Contributor, LibWithMetadata} from '../api'; +import {LibWithMetadata} from '../api'; import companiesDesktopAsset from '../assets/companies-desktop.svg'; import companiesMobileAsset from '../assets/companies-mobile.svg'; import companiesTabletAsset from '../assets/companies-tablet.svg'; @@ -20,12 +20,10 @@ import {CustomPageContent} from './types'; export const getRtlLanding = ({ t, libs, - contributors, backgroundImageSrc, }: { t: TFunction; libs: LibWithMetadata[]; - contributors: Contributor[]; backgroundImageSrc: string; }): CustomPageContent => ({ background: { @@ -169,7 +167,6 @@ export const getRtlLanding = ({ title: t('home:contributors_actions_telegram'), href: 'https://t.me/gravity_ui', }, - contributors, }, { type: BlockType.CompaniesBlock, diff --git a/src/content/landing.ts b/src/content/landing.ts index c4c09595b8f9..c931d1ad9991 100644 --- a/src/content/landing.ts +++ b/src/content/landing.ts @@ -1,7 +1,7 @@ import {BlockType} from '@gravity-ui/page-constructor'; import {TFunction} from 'next-i18next'; -import type {Contributor, LibWithMetadata} from '../api'; +import type {LibWithMetadata} from '../api'; import companiesDesktopAsset from '../assets/companies-desktop.svg'; import companiesMobileAsset from '../assets/companies-mobile.svg'; import companiesTabletAsset from '../assets/companies-tablet.svg'; @@ -20,12 +20,10 @@ import {CustomPageContent} from './types'; export const getLanding = ({ t, libs, - contributors, backgroundImageSrc, }: { t: TFunction; libs: LibWithMetadata[]; - contributors: Contributor[]; backgroundImageSrc: string; }): CustomPageContent => ({ background: { @@ -185,7 +183,6 @@ export const getLanding = ({ title: t('home:contributors_actions_telegram'), href: 'https://t.me/gravity_ui', }, - contributors, }, { type: BlockType.CompaniesBlock, diff --git a/src/pages/api/contributors.ts b/src/pages/api/contributors.ts new file mode 100644 index 000000000000..5e8b4cc84d42 --- /dev/null +++ b/src/pages/api/contributors.ts @@ -0,0 +1,14 @@ +import type {NextApiRequest, NextApiResponse} from 'next'; +import {ServerApi} from 'src/api'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'GET') { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + return; + } + + const contributors = await ServerApi.instance.fetchAllContributorsWithCache(); + + res.status(200).json({contributors}); +} diff --git a/src/pages/components/[libId]/[componentId].tsx b/src/pages/components/[libId]/[componentId].tsx index 84140f4efcbd..4269997c6cae 100644 --- a/src/pages/components/[libId]/[componentId].tsx +++ b/src/pages/components/[libId]/[componentId].tsx @@ -5,7 +5,7 @@ import React from 'react'; import {Section} from 'src/components/NavigationLayout/types'; import i18nextConfig from '../../../../next-i18next.config'; -import {Api, type LibWithFullData} from '../../../api'; +import {type LibWithFullData, ServerApi} from '../../../api'; import {Component} from '../../../components/Component/Component'; import {ComponentsLayout} from '../../../components/ComponentsLayout/ComponentsLayout'; import {Layout} from '../../../components/Layout/Layout'; @@ -34,9 +34,9 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { const libId = ctx.params?.libId as string; - const libPromise = Api.instance.fetchLibByIdWithCache(libId); + const libPromise = ServerApi.instance.fetchLibByIdWithCache(libId); const i18nPropsPromise = getI18nProps(ctx, ['component', 'libraries-info', 'component-meta']); - const readmePromise = Api.instance.fetchComponentReadmeWithCache({ + const readmePromise = ServerApi.instance.fetchComponentReadmeWithCache({ readmeUrl: component.content.readmeUrl, componentId: component.id, libId, diff --git a/src/pages/health.tsx b/src/pages/health.tsx index a4359d751921..4ed373f94687 100644 --- a/src/pages/health.tsx +++ b/src/pages/health.tsx @@ -1,13 +1,13 @@ import {GetServerSideProps} from 'next'; -import {Api} from 'src/api'; +import {ClientApi, ServerApi} from 'src/api'; let cachePromise: Promise | null = null; export const getServerSideProps: GetServerSideProps = async ({res}) => { if (!cachePromise) { cachePromise = Promise.all([ - Api.instance.fetchAllContributorsWithCache(), - Api.instance.fetchAllLibs(), + ClientApi.instance.fetchAllContributors(), + ServerApi.instance.fetchAllLibs(), ]).catch(() => { cachePromise = null; }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f79aa721265f..7eaa431da62b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,30 +1,28 @@ import {GetServerSideProps} from 'next'; import Head from 'next/head'; -import type {Contributor, LibWithMetadata} from '../api'; -import {Api} from '../api'; +import type {LibWithMetadata} from '../api'; +import {ServerApi} from '../api'; import backgroundAsset from '../assets/background.jpg'; import {Landing} from '../components/Landing/Landing'; import {Layout} from '../components/Layout/Layout'; import {getI18nProps} from '../utils/i18next'; export const getServerSideProps: GetServerSideProps = async (ctx) => { - const [contributors, libs, i18nProps] = await Promise.all([ - Api.instance.fetchAllContributorsWithCache(), - Api.instance.fetchLandingLibs(), + const [libs, i18nProps] = await Promise.all([ + ServerApi.instance.fetchLandingLibs(), getI18nProps(ctx, ['home', 'libraries-info']), ]); return { props: { - contributors, libs, ...i18nProps, }, }; }; -const Home = ({libs, contributors}: {libs: LibWithMetadata[]; contributors: Contributor[]}) => { +const Home = ({libs}: {libs: LibWithMetadata[]}) => { return ( <> @@ -32,11 +30,7 @@ const Home = ({libs, contributors}: {libs: LibWithMetadata[]; contributors: Cont - + ); diff --git a/src/pages/libraries/[libId]/index.tsx b/src/pages/libraries/[libId]/index.tsx index 79ff783712f9..e70833e63afc 100644 --- a/src/pages/libraries/[libId]/index.tsx +++ b/src/pages/libraries/[libId]/index.tsx @@ -1,7 +1,7 @@ import {GetServerSideProps} from 'next'; import {useTranslation} from 'next-i18next'; -import {Api, type LibWithFullData} from '../../../api'; +import {type LibWithFullData, ServerApi} from '../../../api'; import {Layout} from '../../../components/Layout/Layout'; import {Library} from '../../../components/Library/Library'; import {getI18nProps, getLibraryMeta, isValidLibId} from '../../../utils'; @@ -16,7 +16,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { } const [lib, i18nProps] = await Promise.all([ - Api.instance.fetchLibByIdWithCache(libId), + ServerApi.instance.fetchLibByIdWithCache(libId), getI18nProps(ctx, ['library', 'libraries-info']), ]); diff --git a/src/pages/libraries/[libId]/preview/index.tsx b/src/pages/libraries/[libId]/preview/index.tsx index b8c2a097edb2..9775967ea538 100644 --- a/src/pages/libraries/[libId]/preview/index.tsx +++ b/src/pages/libraries/[libId]/preview/index.tsx @@ -3,7 +3,7 @@ import {GetServerSideProps} from 'next'; import Head from 'next/head'; import React from 'react'; -import {Api, type LibWithMetadata} from '../../../../api'; +import {type LibWithMetadata, ServerApi} from '../../../../api'; import {LibraryPreview} from '../../../../components/LibraryPreview/LibraryPreview'; import {getI18nProps, isValidLibId} from '../../../../utils'; @@ -19,7 +19,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { } const [lib, i18nProps] = await Promise.all([ - Api.instance.fetchLibByIdWithCache(libId), + ServerApi.instance.fetchLibByIdWithCache(libId), getI18nProps(ctx, ['library', 'libraries-info']), ]); diff --git a/src/pages/libraries/index.tsx b/src/pages/libraries/index.tsx index cb8caf6cc333..6d9fe363bb4d 100644 --- a/src/pages/libraries/index.tsx +++ b/src/pages/libraries/index.tsx @@ -1,14 +1,14 @@ import {GetServerSideProps} from 'next'; import {useTranslation} from 'next-i18next'; -import {Api, type LibWithMetadata} from '../../api'; +import {type LibWithMetadata, ServerApi} from '../../api'; import {Layout} from '../../components/Layout/Layout'; import {Libraries} from '../../components/Libraries/Libraries'; import {getI18nProps} from '../../utils/i18next'; export const getServerSideProps: GetServerSideProps = async (ctx) => { const [libs, i18nProps] = await Promise.all([ - Api.instance.fetchAllLibsOnlyWithMetadata(), + ServerApi.instance.fetchAllLibsOnlyWithMetadata(), getI18nProps(ctx, ['libraries', 'libraries-info']), ]); diff --git a/src/pages/rtl.tsx b/src/pages/rtl.tsx index b524237d0281..b0e992e05e65 100644 --- a/src/pages/rtl.tsx +++ b/src/pages/rtl.tsx @@ -4,35 +4,27 @@ import Head from 'next/head'; import React from 'react'; import nextI18nextConfig from '../../next-i18next.config'; -import {Api, Contributor, LibWithMetadata} from '../api'; +import {LibWithMetadata, ServerApi} from '../api'; import backgroundAsset from '../assets/background.jpg'; import {Landing} from '../components/Landing/Landing'; import {Layout} from '../components/Layout/Layout'; import {getI18nProps} from '../utils/i18next'; export const getServerSideProps: GetServerSideProps = async (ctx) => { - const [contributors, libs, i18nProps] = await Promise.all([ - Api.instance.fetchAllContributorsWithCache(), - Api.instance.fetchLandingLibs(), + const [libs, i18nProps] = await Promise.all([ + ServerApi.instance.fetchLandingLibs(), getI18nProps(ctx, ['home', 'libraries-info']), ]); return { props: { - contributors, libs, ...i18nProps, }, }; }; -export const RTLPage = ({ - libs, - contributors, -}: { - libs: LibWithMetadata[]; - contributors: Contributor[]; -}) => { +export const RTLPage = ({libs}: {libs: LibWithMetadata[]}) => { const {i18n} = useTranslation(); i18n.changeLanguage(nextI18nextConfig.i18n.defaultLocale); @@ -43,11 +35,7 @@ export const RTLPage = ({ - + );