diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index 9b25446c..c6e22a30 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -14,12 +14,14 @@ import { Organization } from '@prisma/client'; import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto'; import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto'; import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service'; +import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; @Controller('/posts') export class PostsController { constructor( private _postsService: PostsService, - private _commentsService: CommentsService + private _commentsService: CommentsService, + private _starsService: StarsService ) {} @Get('/') @@ -42,6 +44,11 @@ export class PostsController { }; } + @Get('/predict-trending') + predictTrending() { + return this._starsService.predictTrending(); + } + @Get('/old') oldPosts( @GetOrgFromRequest() org: Organization, diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index cc751b5f..640b7115 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { FC, useCallback, useEffect, useState } from 'react'; +import React, {FC, MouseEventHandler, useCallback, useEffect, useState} from 'react'; import dayjs from 'dayjs'; import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; import clsx from 'clsx'; @@ -159,6 +159,13 @@ export const AddEditModal: FC<{ // sometimes it's easier to click escape to close useKeypress('Escape', askClose); + const postNow = useCallback(((e) => { + e.stopPropagation(); + e.preventDefault(); + + return schedule('now')(); + }) as MouseEventHandler, []); + // function to send to the server and save const schedule = useCallback( (type: 'draft' | 'now' | 'schedule' | 'delete') => async () => { @@ -363,7 +370,7 @@ export const AddEditModal: FC<{
-
+
{!!existingData.integration && (
diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index e4d617f1..019db9e3 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -28,6 +28,7 @@ const CalendarContext = createContext({ currentYear: dayjs().year(), comments: [] as Array<{ date: string; total: number }>, integrations: [] as Integrations[], + trendings: [] as string[], posts: [] as Array, setFilters: (filters: { currentWeek: number; currentYear: number }) => {}, changeDate: (id: string, date: dayjs.Dayjs) => {}, @@ -46,8 +47,15 @@ export const CalendarWeekProvider: FC<{ }> = ({ children, integrations }) => { const fetch = useFetch(); const [internalData, setInternalData] = useState([] as any[]); + const [trendings, setTrendings] = useState([]); const { mutate } = useSWRConfig(); + useEffect(() => { + (async () => { + setTrendings(await (await fetch('/posts/predict-trending')).json()); + })(); + }, []); + const [filters, setFilters] = useState({ currentWeek: dayjs().week(), currentYear: dayjs().year(), @@ -72,12 +80,18 @@ export const CalendarWeekProvider: FC<{ const loadData = useCallback( async (url: string) => { - return (await fetch(`${url}?${params}`)).json(); + const data = (await fetch(`${url}?${params}`)).json(); + return data; }, [filters] ); - const swr = useSWR(`/posts`, loadData); + const swr = useSWR(`/posts`, loadData, { + refreshInterval: 3600000, + refreshWhenOffline: false, + refreshWhenHidden: false, + revalidateOnFocus: false, + }); const { isLoading } = swr; const { posts, comments } = swr?.data || { posts: [], comments: [] }; @@ -86,7 +100,7 @@ export const CalendarWeekProvider: FC<{ setInternalData((d) => d.map((post: Post) => { if (post.id === id) { - return { ...post, publishDate: date.format('YYYY-MM-DDTHH:mm:ss') }; + return { ...post, publishDate: date.utc().format('YYYY-MM-DDTHH:mm:ss') }; } return post; }) @@ -101,9 +115,11 @@ export const CalendarWeekProvider: FC<{ } }, [posts]); + return ( { const CalendarColumn: FC<{ day: number; hour: string }> = (props) => { const { day, hour } = props; - const { currentWeek, currentYear, integrations, posts, changeDate } = - useCalendar(); + const { currentWeek, currentYear } = useCalendar(); + + const getDate = useMemo(() => { + const date = + dayjs() + .year(currentYear) + .isoWeek(currentWeek) + .isoWeekday(day) + .format('YYYY-MM-DD') + + 'T' + + hour + + ':00'; + return dayjs(date); + }, [currentWeek]); + + const isBeforeNow = useMemo(() => { + return getDate.isBefore(dayjs()); + }, [getDate]); + + const [ref, entry] = useIntersectionObserver({ + threshold: 0.5, + root: null, + rootMargin: '0px', + }); + + return ( +
+ {!entry?.isIntersecting ? ( +
+ {!isBeforeNow && ( +
+ + +
+ )} +
+ ) : ( + + )} +
+ ); +}; +const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => { + const { day, hour } = props; + const { + currentWeek, + currentYear, + integrations, + posts, + trendings, + changeDate, + } = useCalendar(); + const modal = useModals(); const fetch = useFetch(); @@ -159,10 +222,22 @@ const CalendarColumn: FC<{ day: number; hour: string }> = (props) => { const postList = useMemo(() => { return posts.filter((post) => { - return dayjs(post.publishDate).local().isSame(getDate); + return dayjs + .utc(post.publishDate) + .local() + .isBetween(getDate, getDate.add(10, 'minute'), 'minute', '[)'); }); }, [posts]); + const canBeTrending = useMemo(() => { + return !!trendings.find((trend) => { + return dayjs + .utc(trend) + .local() + .isBetween(getDate, getDate.add(10, 'minute'), 'minute', '[)'); + }); + }, [trendings]); + const isBeforeNow = useMemo(() => { return getDate.isBefore(dayjs()); }, [getDate]); @@ -232,11 +307,18 @@ const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
{postList.map((post) => ( @@ -351,7 +433,13 @@ export const CommentBox: FC<{ totalComments: number; date: dayjs.Dayjs }> = ( }, [date]); return ( -
+
{ }, [week.currentWeek, week.currentYear]); return ( -
+
-
+

Channels

diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index 9a3bdfa5..eec43af9 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -154,7 +154,7 @@ export class PostsRepository { } async createOrUpdatePost( - state: 'draft' | 'schedule', + state: 'draft' | 'schedule' | 'now', orgId: string, date: string, body: PostBody diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 494e7835..5cff8904 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -163,7 +163,7 @@ export class PostsService { await this._postRepository.createOrUpdatePost( body.type, orgId, - body.date, + body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date, post ); @@ -172,11 +172,11 @@ export class PostsService { 'post', previousPost ? previousPost : posts?.[0]?.id ); - if (body.type === 'schedule' && dayjs(body.date).isAfter(dayjs())) { + if ((body.type === 'schedule' || body.type === 'now') && dayjs(body.date).isAfter(dayjs())) { this._workerServiceProducer.emit('post', { id: posts[0].id, options: { - delay: 0, //dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'), + delay: body.type === 'now' ? 0 : dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'), }, payload: { id: posts[0].id, diff --git a/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts b/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts index d19aa0d3..cfba6768 100644 --- a/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts @@ -1,231 +1,373 @@ -import {Injectable} from "@nestjs/common"; -import {StarsRepository} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.repository"; -import {chunk, groupBy} from "lodash"; -import dayjs from "dayjs"; -import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service"; -import {StarsListDto} from "@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto"; -import {BullMqClient} from "@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client"; +import { Injectable } from '@nestjs/common'; +import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository'; +import { chunk, groupBy } from 'lodash'; +import dayjs from 'dayjs'; +import { NotificationService } from '@gitroom/nestjs-libraries/notifications/notification.service'; +import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto'; +import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client'; +import { mean } from 'simple-statistics'; enum Inform { - Removed, - New, - Changed + Removed, + New, + Changed, } @Injectable() export class StarsService { - constructor( - private _starsRepository: StarsRepository, - private _notificationsService: NotificationService, - private _workerServiceProducer: BullMqClient - ){} + constructor( + private _starsRepository: StarsRepository, + private _notificationsService: NotificationService, + private _workerServiceProducer: BullMqClient + ) {} - getGitHubRepositoriesByOrgId(org: string) { - return this._starsRepository.getGitHubRepositoriesByOrgId(org); + getGitHubRepositoriesByOrgId(org: string) { + return this._starsRepository.getGitHubRepositoriesByOrgId(org); + } + + getAllGitHubRepositories() { + return this._starsRepository.getAllGitHubRepositories(); + } + + getStarsByLogin(login: string) { + return this._starsRepository.getStarsByLogin(login); + } + + getLastStarsByLogin(login: string) { + return this._starsRepository.getLastStarsByLogin(login); + } + + createStars( + login: string, + totalNewsStars: number, + totalStars: number, + date: Date + ) { + return this._starsRepository.createStars( + login, + totalNewsStars, + totalStars, + date + ); + } + + async sync(login: string) { + const loadAllStars = await this.syncProcess(login); + const sortedArray = Object.keys(loadAllStars).sort( + (a, b) => dayjs(a).unix() - dayjs(b).unix() + ); + let addPreviousStars = 0; + for (const date of sortedArray) { + const dateObject = dayjs(date).toDate(); + addPreviousStars += loadAllStars[date]; + await this._starsRepository.createStars( + login, + loadAllStars[date], + addPreviousStars, + dateObject + ); + } + } + + async syncProcess(login: string, page = 1) { + const starsRequest = await fetch( + `https://api.github.com/repos/${login}/stargazers?page=${page}&per_page=100`, + { + headers: { + Accept: 'application/vnd.github.v3.star+json', + ...(process.env.GITHUB_AUTH + ? { Authorization: `token ${process.env.GITHUB_AUTH}` } + : {}), + }, + } + ); + const totalRemaining = +( + starsRequest.headers.get('x-ratelimit-remaining') || + starsRequest.headers.get('X-RateLimit-Remaining') || + 0 + ); + const resetTime = +( + starsRequest.headers.get('x-ratelimit-reset') || + starsRequest.headers.get('X-RateLimit-Reset') || + 0 + ); + + if (totalRemaining < 10) { + console.log('waiting for the rate limit'); + const delay = resetTime * 1000 - Date.now() + 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); } - getAllGitHubRepositories() { - return this._starsRepository.getAllGitHubRepositories(); + const data: Array<{ starred_at: string }> = await starsRequest.json(); + const mapDataToDate = groupBy(data, (p) => + dayjs(p.starred_at).format('YYYY-MM-DD') + ); + + // take all the stars from the page + const aggStars: { [key: string]: number } = Object.values( + mapDataToDate + ).reduce( + (acc, value) => ({ + ...acc, + [value[0].starred_at]: value.length, + }), + {} + ); + + // if we have 100 stars, we need to fetch the next page and merge the results (recursively) + const nextOne: { [key: string]: number } = + data.length === 100 ? await this.syncProcess(login, page + 1) : {}; + + // merge the results + const allKeys = [ + ...new Set([...Object.keys(aggStars), ...Object.keys(nextOne)]), + ]; + + return { + ...allKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: (aggStars[key] || 0) + (nextOne[key] || 0), + }), + {} as { [key: string]: number } + ), + }; + } + + async updateTrending( + language: string, + hash: string, + arr: Array<{ name: string; position: number }> + ) { + const currentTrending = await this._starsRepository.getTrendingByLanguage( + language + ); + if (currentTrending?.hash === hash) { + return; + } + await this.newTrending(language); + if (currentTrending) { + const list: Array<{ name: string; position: number }> = JSON.parse( + currentTrending.trendingList + ); + const removedFromTrending = list.filter( + (p) => !arr.find((a) => a.name === p.name) + ); + const changedPosition = arr.filter((p) => { + const current = list.find((a) => a.name === p.name); + return current && current.position !== p.position; + }); + if (removedFromTrending.length) { + // let people know they are not trending anymore + await this.inform(Inform.Removed, removedFromTrending, language); + } + if (changedPosition.length) { + // let people know they changed position + await this.inform(Inform.Changed, changedPosition, language); + } } - getStarsByLogin(login: string) { - return this._starsRepository.getStarsByLogin(login); - } + const informNewPeople = arr.filter( + (p) => currentTrending?.trendingList?.indexOf(p.name) === -1 + ); - getLastStarsByLogin(login: string) { - return this._starsRepository.getLastStarsByLogin(login); - } + // let people know they are trending + await this.inform(Inform.New, informNewPeople, language); + await this.replaceOrAddTrending(language, hash, arr); + } - createStars(login: string, totalNewsStars: number, totalStars: number, date: Date) { - return this._starsRepository.createStars(login, totalNewsStars, totalStars, date); - } - - async sync(login: string) { - const loadAllStars = await this.syncProcess(login); - const sortedArray = Object.keys(loadAllStars).sort((a, b) => dayjs(a).unix() - dayjs(b).unix()); - let addPreviousStars = 0; - for (const date of sortedArray) { - const dateObject = dayjs(date).toDate(); - addPreviousStars += loadAllStars[date]; - await this._starsRepository.createStars(login, loadAllStars[date], addPreviousStars, dateObject); + async inform( + type: Inform, + removedFromTrending: Array<{ name: string; position: number }>, + language: string + ) { + const names = await this._starsRepository.getGitHubsByNames( + removedFromTrending.map((p) => p.name) + ); + const mapDbNamesToList = names.map( + (n) => removedFromTrending.find((p) => p.name === n.login)! + ); + for (const person of mapDbNamesToList) { + const getOrganizationsByGitHubLogin = + await this._starsRepository.getOrganizationsByGitHubLogin(person.name); + for (const org of getOrganizationsByGitHubLogin) { + const topic = `organization:${org.organizationId}`; + switch (type) { + case Inform.Removed: + return this._notificationsService.sendNotificationToTopic( + 'trending', + topic, + { message: `You are not trending anymore in ${language}` } + ); + case Inform.New: + return this._notificationsService.sendNotificationToTopic( + 'trending', + topic, + { + message: `You are trending in ${ + language || 'On the main feed' + } position #${person.position}`, + } + ); + case Inform.Changed: + return this._notificationsService.sendNotificationToTopic( + 'trending', + topic, + { + message: `You changed position in ${ + language || 'On the main feed' + } position #${person.position}`, + } + ); } + } } + } - async syncProcess(login: string, page = 1) { - const starsRequest = await fetch(`https://api.github.com/repos/${login}/stargazers?page=${page}&per_page=100`, { - headers: { - Accept: 'application/vnd.github.v3.star+json', - ...process.env.GITHUB_AUTH ? {Authorization: `token ${process.env.GITHUB_AUTH}`} : {} - } - }); - const totalRemaining = +(starsRequest.headers.get('x-ratelimit-remaining') || starsRequest.headers.get('X-RateLimit-Remaining') || 0); - const resetTime = +(starsRequest.headers.get('x-ratelimit-reset') || starsRequest.headers.get('X-RateLimit-Reset') || 0); + async replaceOrAddTrending( + language: string, + hash: string, + arr: Array<{ name: string; position: number }> + ) { + return this._starsRepository.replaceOrAddTrending(language, hash, arr); + } - if (totalRemaining < 10) { - console.log('waiting for the rate limit'); - const delay = (resetTime * 1000) - Date.now() + 1000; - await new Promise(resolve => setTimeout(resolve, delay)); - } + async newTrending(language: string) { + return this._starsRepository.newTrending(language); + } - const data: Array<{starred_at: string}> = await starsRequest.json(); - const mapDataToDate = groupBy(data, (p) => dayjs(p.starred_at).format('YYYY-MM-DD')); + async getStars(org: string) { + const getGitHubs = await this.getGitHubRepositoriesByOrgId(org); + const list = []; + for (const gitHub of getGitHubs) { + if (!gitHub.login) { + continue; + } + const stars = await this.getStarsByLogin(gitHub.login!); + const graphSize = stars.length < 10 ? stars.length : stars.length / 10; - // take all the stars from the page - const aggStars: {[key: string]: number} = Object.values(mapDataToDate).reduce((acc, value) => ({ + list.push({ + login: gitHub.login, + stars: chunk(stars, graphSize).reduce((acc, chunkedStars) => { + return [ ...acc, - [value[0].starred_at]: value.length, - }), {}); - - // if we have 100 stars, we need to fetch the next page and merge the results (recursively) - const nextOne: {[key: string]: number} = (data.length === 100) ? await this.syncProcess(login, page + 1) : {}; - - // merge the results - const allKeys = [...new Set([...Object.keys(aggStars), ...Object.keys(nextOne)])]; - - return { - ...allKeys.reduce((acc, key) => ({ - ...acc, - [key]: (aggStars[key] || 0) + (nextOne[key] || 0) - }), {} as {[key: string]: number}) - }; - } - - async updateTrending(language: string, hash: string, arr: Array<{name: string, position: number}>) { - const currentTrending = await this._starsRepository.getTrendingByLanguage(language); - if (currentTrending?.hash === hash) { - return; - } - await this.newTrending(language); - if (currentTrending) { - const list: Array<{name: string, position: number}> = JSON.parse(currentTrending.trendingList); - const removedFromTrending = list.filter(p => !arr.find(a => a.name === p.name)); - const changedPosition = arr.filter(p => { - const current = list.find(a => a.name === p.name); - return current && current.position !== p.position; - }); - if (removedFromTrending.length) { - // let people know they are not trending anymore - await this.inform(Inform.Removed, removedFromTrending, language); - } - if (changedPosition.length) { - // let people know they changed position - await this.inform(Inform.Changed, changedPosition, language); - } - } - - const informNewPeople = arr.filter(p => currentTrending?.trendingList?.indexOf(p.name) === -1); - - // let people know they are trending - await this.inform(Inform.New, informNewPeople, language); - await this.replaceOrAddTrending(language, hash, arr); - } - - async inform(type: Inform, removedFromTrending: Array<{name: string, position: number}>, language: string) { - const names = await this._starsRepository.getGitHubsByNames(removedFromTrending.map(p => p.name)); - const mapDbNamesToList = names.map(n => removedFromTrending.find(p => p.name === n.login)!); - for (const person of mapDbNamesToList) { - const getOrganizationsByGitHubLogin = await this._starsRepository.getOrganizationsByGitHubLogin(person.name); - for (const org of getOrganizationsByGitHubLogin) { - const topic = `organization:${org.organizationId}`; - switch (type) { - case Inform.Removed: - return this._notificationsService.sendNotificationToTopic('trending', topic, {message: `You are not trending anymore in ${language}`}); - case Inform.New: - return this._notificationsService.sendNotificationToTopic('trending', topic, {message: `You are trending in ${language || 'On the main feed'} position #${person.position}`}); - case Inform.Changed: - return this._notificationsService.sendNotificationToTopic( 'trending', topic, {message: `You changed position in ${language || 'On the main feed'} position #${person.position}`}); - } - } - } - } - - async replaceOrAddTrending(language: string, hash: string, arr: Array<{name: string, position: number}>) { - return this._starsRepository.replaceOrAddTrending(language, hash, arr); - } - - async newTrending(language: string) { - return this._starsRepository.newTrending(language); - } - - async getStars(org: string) { - const getGitHubs = await this.getGitHubRepositoriesByOrgId(org); - const list = []; - for (const gitHub of getGitHubs) { - if (!gitHub.login) { - continue; - } - const stars = await this.getStarsByLogin(gitHub.login!); - const graphSize = stars.length < 10 ? stars.length : stars.length / 10; - - list.push({ - login: gitHub.login, - stars: chunk(stars, graphSize).reduce((acc, chunkedStars) => { - return [ - ...acc, - { - totalStars: chunkedStars[chunkedStars.length - 1].totalStars, - date: chunkedStars[chunkedStars.length - 1].date - } - ] - }, [] as Array<{totalStars: number, date: Date}>) - }); - } - - return list; - } - - async getTrending(language: string) { - return this._starsRepository.getLastTrending(language); - } - - async getStarsFilter(orgId: string, starsFilter: StarsListDto) { - const getGitHubs = await this.getGitHubRepositoriesByOrgId(orgId); - if (getGitHubs.filter(f => f.login).length === 0) { - return []; - } - return this._starsRepository.getStarsFilter(getGitHubs.map(p => p.login) as string[], starsFilter); - } - - async addGitHub(orgId: string, code: string) { - const {access_token} = await (await fetch('https://github.com/login/oauth/access_token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' + { + totalStars: chunkedStars[chunkedStars.length - 1].totalStars, + date: chunkedStars[chunkedStars.length - 1].date, }, - body: JSON.stringify({ - client_id: process.env.GITHUB_CLIENT_ID, - client_secret: process.env.GITHUB_CLIENT_SECRET, - code, - redirect_uri: `${process.env.FRONTEND_URL}/settings` - }) - })).json(); - - return this._starsRepository.addGitHub(orgId, access_token); + ]; + }, [] as Array<{ totalStars: number; date: Date }>), + }); } - async getOrganizations(orgId: string, id: string) { - const getGitHub = await this._starsRepository.getGitHubById(orgId, id); - return (await fetch(`https://api.github.com/user/orgs`, { - headers: { - Authorization: `token ${getGitHub?.token!}` - } - })).json(); + return list; + } + + async getTrending(language: string) { + return this._starsRepository.getLastTrending(language); + } + + async getStarsFilter(orgId: string, starsFilter: StarsListDto) { + const getGitHubs = await this.getGitHubRepositoriesByOrgId(orgId); + if (getGitHubs.filter((f) => f.login).length === 0) { + return []; + } + return this._starsRepository.getStarsFilter( + getGitHubs.map((p) => p.login) as string[], + starsFilter + ); + } + + async addGitHub(orgId: string, code: string) { + const { access_token } = await ( + await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + code, + redirect_uri: `${process.env.FRONTEND_URL}/settings`, + }), + }) + ).json(); + + return this._starsRepository.addGitHub(orgId, access_token); + } + + async getOrganizations(orgId: string, id: string) { + const getGitHub = await this._starsRepository.getGitHubById(orgId, id); + return ( + await fetch(`https://api.github.com/user/orgs`, { + headers: { + Authorization: `token ${getGitHub?.token!}`, + }, + }) + ).json(); + } + + async getRepositoriesOfOrganization( + orgId: string, + id: string, + github: string + ) { + const getGitHub = await this._starsRepository.getGitHubById(orgId, id); + return ( + await fetch(`https://api.github.com/orgs/${github}/repos`, { + headers: { + Authorization: `token ${getGitHub?.token!}`, + }, + }) + ).json(); + } + + async updateGitHubLogin(orgId: string, id: string, login: string) { + this._workerServiceProducer + .emit('sync_all_stars', { payload: { login } }) + .subscribe(); + return this._starsRepository.updateGitHubLogin(orgId, id, login); + } + + async deleteRepository(orgId: string, id: string) { + return this._starsRepository.deleteRepository(orgId, id); + } + + async predictTrending() { + const trendings = (await this.getTrending('')).reverse(); + const dates = await this.predictTrendingLoop(trendings); + return dates.map(d => dayjs(d).format('YYYY-MM-DDTHH:mm:00')); + } + + async predictTrendingLoop(trendings: Array<{ date: Date }>, current = 0): Promise { + const dates = trendings.map((result) => dayjs(result.date).toDate()); + const intervals = dates + .slice(1) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + .map((date, i) => (date - dates[i]) / (1000 * 60 * 60 * 24)); + const nextInterval = intervals.length === 0 ? null : mean(intervals); + const lastTrendingDate = dates[dates.length - 1]; + const nextTrendingDate = !nextInterval + ? false + : dayjs( + new Date( + lastTrendingDate.getTime() + nextInterval * 24 * 60 * 60 * 1000 + ) + ).toDate(); + + if (!nextTrendingDate) { + return []; } - async getRepositoriesOfOrganization(orgId: string, id: string, github: string) { - const getGitHub = await this._starsRepository.getGitHubById(orgId, id); - return (await fetch(`https://api.github.com/orgs/${github}/repos`, { - headers: { - Authorization: `token ${getGitHub?.token!}` - } - })).json(); - } - - async updateGitHubLogin(orgId: string, id: string, login: string) { - this._workerServiceProducer.emit('sync_all_stars', {payload: {login}}).subscribe(); - return this._starsRepository.updateGitHubLogin(orgId, id, login); - } - - async deleteRepository(orgId: string, id: string) { - return this._starsRepository.deleteRepository(orgId, id); - } -} \ No newline at end of file + return [ + nextTrendingDate, + ...(current < 500 + ? await this.predictTrendingLoop( + [...trendings, { date: nextTrendingDate }], + current + 1 + ) + : []), + ]; + } +} diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts index c554402a..c939dee2 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts @@ -68,8 +68,8 @@ export class Post { export class CreatePostDto { @IsDefined() - @IsIn(['draft', 'schedule']) - type: 'draft' | 'schedule'; + @IsIn(['draft', 'schedule', 'now']) + type: 'draft' | 'schedule' | 'now'; @IsDefined() @IsDateString() diff --git a/libraries/nestjs-libraries/src/services/trending.service.ts b/libraries/nestjs-libraries/src/services/trending.service.ts index 4484856c..ba7ada04 100644 --- a/libraries/nestjs-libraries/src/services/trending.service.ts +++ b/libraries/nestjs-libraries/src/services/trending.service.ts @@ -23,6 +23,7 @@ export class TrendingService { }); const hashedNames = md5(arr.map(p => p.name).join('')); + console.log(language, hashedNames); await this._starsService.updateTrending(language.name, hashedNames, arr); } } diff --git a/package-lock.json b/package-lock.json index 9b396591..3dd448cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@prisma/client": "^5.8.1", "@swc/helpers": "~0.5.2", "@sweetalert2/theme-dark": "^5.0.16", + "@tanstack/react-virtual": "^3.1.3", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.202", @@ -34,7 +35,9 @@ "@types/mime-types": "^2.1.4", "@types/remove-markdown": "^0.3.4", "@types/stripe": "^8.0.417", + "@uidotdev/usehooks": "^2.4.1", "@uiw/react-md-editor": "^4.0.3", + "@virtual-grid/react": "^2.0.2", "axios": "^1.0.0", "bcrypt": "^5.1.1", "bullmq": "^5.1.5", @@ -4135,6 +4138,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" + }, "node_modules/@kurkle/color": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", @@ -6929,6 +6937,31 @@ } } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.1.3.tgz", + "integrity": "sha512-YCzcbF/Ws/uZ0q3Z6fagH+JVhx4JLvbSflgldMgLsuvB8aXjZLLb3HvrEVxY480F9wFlBiXlvQxOyXb5ENPrNA==", + "dependencies": { + "@tanstack/virtual-core": "3.1.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.1.3.tgz", + "integrity": "sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", @@ -7935,6 +7968,18 @@ "@ucast/mongo": "^2.4.0" } }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@uiw/copy-to-clipboard": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.16.tgz", @@ -7989,6 +8034,32 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@virtual-grid/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@virtual-grid/core/-/core-2.0.1.tgz", + "integrity": "sha512-ZqhtuVGMVM89W/8PFljGqaWV0f8Jce+x68y4hWXpgCHlhSwpGrAKE5awgG1qHDsbTcM0kv1cL88KOFWknbnyWQ==" + }, + "node_modules/@virtual-grid/react": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@virtual-grid/react/-/react-2.0.2.tgz", + "integrity": "sha512-cwlMlTdJTX+BhlE1PhgLQgWzHkYCf722ZCwp/56caMg2sGjXloaQDWU+jUk6g2TFQ/mlteC1w7aYkPZC+ZwxWg==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.1", + "@virtual-grid/core": "^2.0.1", + "@virtual-grid/shared": "^2.0.1", + "react-intersection-observer": "^9.5.2", + "use-deep-compare": "^1.2.1", + "use-resize-observer": "^9.1.0" + }, + "peerDependencies": { + "react": "^17.0.2 || ^18.0.0" + } + }, + "node_modules/@virtual-grid/shared": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@virtual-grid/shared/-/shared-2.0.1.tgz", + "integrity": "sha512-E0krspmtVOGRg/qAgDKUjhTRV7VXmFp7Q05ljT87Llffh8g5JoXVsAUPV7JiRKnrSRFShpiVdCy9eq0VvVeifA==" + }, "node_modules/@vitejs/plugin-react": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", @@ -20387,6 +20458,20 @@ "react": ">=16.0.0" } }, + "node_modules/react-intersection-observer": { + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.8.1.tgz", + "integrity": "sha512-QzOFdROX8D8MH3wE3OVKH0f3mLjKTtEN1VX/rkNuECCff+aKky0pIjulDhr3Ewqj5el/L+MhBkM3ef0Tbt+qUQ==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -23843,6 +23928,17 @@ "react": ">=16.8.0" } }, + "node_modules/use-deep-compare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.2.1.tgz", + "integrity": "sha512-JTnOZAr0fq1ix6CQ4XANoWIh03xAiMFlP/lVAYDdAOZwur6nqBSdATn1/Q9PLIGIW+C7xmFZBCcaA4KLDcQJtg==", + "dependencies": { + "dequal": "2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", @@ -23872,6 +23968,18 @@ } } }, + "node_modules/use-resize-observer": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", + "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==", + "dependencies": { + "@juggle/resize-observer": "^3.3.1" + }, + "peerDependencies": { + "react": "16.8.0 - 18", + "react-dom": "16.8.0 - 18" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 307ff662..98f8ea94 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@prisma/client": "^5.8.1", "@swc/helpers": "~0.5.2", "@sweetalert2/theme-dark": "^5.0.16", + "@tanstack/react-virtual": "^3.1.3", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.202", @@ -34,7 +35,9 @@ "@types/mime-types": "^2.1.4", "@types/remove-markdown": "^0.3.4", "@types/stripe": "^8.0.417", + "@uidotdev/usehooks": "^2.4.1", "@uiw/react-md-editor": "^4.0.3", + "@virtual-grid/react": "^2.0.2", "axios": "^1.0.0", "bcrypt": "^5.1.1", "bullmq": "^5.1.5",