diff --git a/.eslintrc.json b/.eslintrc.json index 30b72661..59107204 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,8 @@ "extends": ["plugin:@nx/typescript"], "rules": { "@typescript-eslint/no-non-null-asserted-optional-chain": "off", - "@typescript-eslint/no-explicit-any": "off" + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off" } }, { diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index d7fbb966..2fa493db 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -3,14 +3,35 @@ import {AuthController} from "@gitroom/backend/api/routes/auth.controller"; import {AuthService} from "@gitroom/backend/services/auth/auth.service"; import {UsersController} from "@gitroom/backend/api/routes/users.controller"; import {AuthMiddleware} from "@gitroom/backend/services/auth/auth.middleware"; +import {StripeController} from "@gitroom/backend/api/routes/stripe.controller"; +import {StripeService} from "@gitroom/nestjs-libraries/services/stripe.service"; +import {AnalyticsController} from "@gitroom/backend/api/routes/analytics.controller"; +import {PoliciesGuard} from "@gitroom/backend/services/auth/permissions/permissions.guard"; +import {PermissionsService} from "@gitroom/backend/services/auth/permissions/permissions.service"; +import {IntegrationsController} from "@gitroom/backend/api/routes/integrations.controller"; +import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integration.manager"; +import {SettingsController} from "@gitroom/backend/api/routes/settings.controller"; const authenticatedController = [ - UsersController + UsersController, + AnalyticsController, + IntegrationsController, + SettingsController ]; @Module({ imports: [], - controllers: [AuthController, ...authenticatedController], - providers: [AuthService], + controllers: [StripeController, AuthController, ...authenticatedController], + providers: [ + AuthService, + StripeService, + AuthMiddleware, + PoliciesGuard, + PermissionsService, + IntegrationManager + ], + get exports() { + return [...this.imports, ...this.providers]; + } }) export class ApiModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/apps/backend/src/api/routes/analytics.controller.ts b/apps/backend/src/api/routes/analytics.controller.ts new file mode 100644 index 00000000..ea6753e0 --- /dev/null +++ b/apps/backend/src/api/routes/analytics.controller.ts @@ -0,0 +1,46 @@ +import {Body, Controller, Get, Post} from '@nestjs/common'; +import {Organization} from "@prisma/client"; +import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request"; +import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; +import dayjs from "dayjs"; +import {mean} from 'simple-statistics'; +import {StarsListDto} from "@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto"; + +@Controller('/analytics') +export class AnalyticsController { + constructor( + private _starsService: StarsService + ) { + } + @Get('/') + async getStars( + @GetOrgFromRequest() org: Organization + ) { + return this._starsService.getStars(org.id); + } + + @Get('/trending') + async getTrending() { + const trendings = (await this._starsService.getTrending('')).reverse(); + const dates = trendings.map(result => dayjs(result.date).toDate()); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const intervals = dates.slice(1).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 ? 'Not possible yet' : dayjs(new Date(lastTrendingDate.getTime() + nextInterval * 24 * 60 * 60 * 1000)).format('YYYY-MM-DD HH:mm:ss'); + + return { + last: dayjs(lastTrendingDate).format('YYYY-MM-DD HH:mm:ss'), + predictions: nextTrendingDate + } + } + + @Post('/stars') + async getStarsFilter( + @GetOrgFromRequest() org: Organization, + @Body() starsFilter: StarsListDto + ) { + return {stars: await this._starsService.getStarsFilter(org.id, starsFilter)}; + } +} diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index 4b336101..6ca2e4b2 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -22,10 +22,13 @@ export class AuthController { domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, secure: true, httpOnly: true, + sameSite: 'none', expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), }); response.header('reload', 'true'); - response.status(200).send(); + response.status(200).json({ + register: true + }); } catch (e) { response.status(400).send(e.message); @@ -44,10 +47,13 @@ export class AuthController { domain: '.' + new URL(process.env.FRONTEND_URL!).hostname, secure: true, httpOnly: true, + sameSite: 'none', expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), }); response.header('reload', 'true'); - response.status(200).send(); + response.status(200).json({ + login: true + }); } catch (e) { response.status(400).send(e.message); diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts new file mode 100644 index 00000000..ca9d9d37 --- /dev/null +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -0,0 +1,82 @@ +import {Body, Controller, Get, Param, Post} from '@nestjs/common'; +import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service"; +import {ConnectIntegrationDto} from "@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto"; +import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integration.manager"; +import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service"; +import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request"; +import {Organization} from "@prisma/client"; + +@Controller('/integrations') +export class IntegrationsController { + constructor( + private _integrationManager: IntegrationManager, + private _integrationService: IntegrationService + ) { + } + @Get('/social/:integration') + async getIntegrationUrl( + @Param('integration') integration: string + ) { + if (!this._integrationManager.getAllowedSocialsIntegrations().includes(integration)) { + throw new Error('Integration not allowed'); + } + + const integrationProvider = this._integrationManager.getSocialIntegration(integration); + const {codeVerifier, state, url} = await integrationProvider.generateAuthUrl(); + await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300); + + return url; + } + + @Post('/article/:integration/connect') + async connectArticle( + @GetOrgFromRequest() org: Organization, + @Param('integration') integration: string, + @Body('code') api: string + ) { + if (!this._integrationManager.getAllowedArticlesIntegrations().includes(integration)) { + throw new Error('Integration not allowed'); + } + + if (!api) { + throw new Error('Missing api'); + } + + const integrationProvider = this._integrationManager.getArticlesIntegration(integration); + const {id, name, token} = await integrationProvider.authenticate(api); + + if (!id) { + throw new Error('Invalid api key'); + } + + return this._integrationService.createIntegration(org.id, name, 'article', String(id), integration, token); + } + + @Post('/social/:integration/connect') + async connectSocialMedia( + @GetOrgFromRequest() org: Organization, + @Param('integration') integration: string, + @Body() body: ConnectIntegrationDto + ) { + if (!this._integrationManager.getAllowedSocialsIntegrations().includes(integration)) { + throw new Error('Integration not allowed'); + } + + const getCodeVerifier = await ioRedis.get(`login:${body.state}`); + if (!getCodeVerifier) { + throw new Error('Invalid state'); + } + + const integrationProvider = this._integrationManager.getSocialIntegration(integration); + const {accessToken, expiresIn, refreshToken, id, name} = await integrationProvider.authenticate({ + code: body.code, + codeVerifier: getCodeVerifier + }); + + if (!id) { + throw new Error('Invalid api key'); + } + + return this._integrationService.createIntegration(org.id, name, 'social', String(id), integration, accessToken, refreshToken, expiresIn); + } +} diff --git a/apps/backend/src/api/routes/settings.controller.ts b/apps/backend/src/api/routes/settings.controller.ts new file mode 100644 index 00000000..6f283597 --- /dev/null +++ b/apps/backend/src/api/routes/settings.controller.ts @@ -0,0 +1,76 @@ +import {Body, Controller, Delete, Get, Param, Post} from "@nestjs/common"; +import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request"; +import {Organization} from "@prisma/client"; +import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; + +@Controller('/settings') +export class SettingsController { + constructor( + private starsService: StarsService, + ) { + } + + @Get('/github') + async getConnectedGithubAccounts( + @GetOrgFromRequest() org: Organization + ) { + return { + github: (await this.starsService.getGitHubRepositoriesByOrgId(org.id)).map((repo) => ({ + id: repo.id, + login: repo.login, + })) + } + } + + @Post('/github') + async addGitHub( + @GetOrgFromRequest() org: Organization, + @Body('code') code: string + ) { + if (!code) { + throw new Error('No code provided'); + } + await this.starsService.addGitHub(org.id, code); + } + + @Get('/github/url') + authUrl() { + return { + url: `https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&scope=${encodeURIComponent('read:org repo')}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/settings`)}` + }; + } + + @Get('/organizations/:id') + async getOrganizations( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string + ) { + return {organizations: await this.starsService.getOrganizations(org.id, id)}; + } + + @Get('/organizations/:id/:github') + async getRepositories( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Param('github') github: string, + ) { + return {repositories: await this.starsService.getRepositoriesOfOrganization(org.id, id, github)}; + } + + @Post('/organizations/:id') + async updateGitHubLogin( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Body('login') login: string, + ) { + return this.starsService.updateGitHubLogin(org.id, id, login); + } + + @Delete('/repository/:id') + async deleteRepository( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string + ) { + return this.starsService.deleteRepository(org.id, id); + } +} \ No newline at end of file diff --git a/apps/backend/src/api/routes/stripe.controller.ts b/apps/backend/src/api/routes/stripe.controller.ts new file mode 100644 index 00000000..abf1dda0 --- /dev/null +++ b/apps/backend/src/api/routes/stripe.controller.ts @@ -0,0 +1,38 @@ +import {Controller, Post, RawBodyRequest, Req} from "@nestjs/common"; +import {StripeService} from "@gitroom/nestjs-libraries/services/stripe.service"; + +@Controller('/stripe') +export class StripeController { + constructor( + private readonly _stripeService: StripeService + ) { + } + @Post('/') + stripe( + @Req() req: RawBodyRequest + ) { + const event = this._stripeService.validateRequest( + req.rawBody, + req.headers['stripe-signature'], + process.env.PAYMENT_SIGNING_SECRET + ); + + // Maybe it comes from another stripe webhook + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (event?.data?.object?.metadata?.service !== 'gitroom') { + return {ok: true}; + } + + switch (event.type) { + case 'customer.subscription.created': + return this._stripeService.createSubscription(event); + case 'customer.subscription.updated': + return this._stripeService.updateSubscription(event); + case 'customer.subscription.deleted': + return this._stripeService.deleteSubscription(event); + default: + return {ok: true}; + } + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 5d613397..b247ae12 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -1,17 +1,21 @@ -import { Module } from '@nestjs/common'; +import {Global, Module} from '@nestjs/common'; import {DatabaseModule} from "@gitroom/nestjs-libraries/database/prisma/database.module"; import {RedisModule} from "@gitroom/nestjs-libraries/redis/redis.module"; -import {AuthService} from "@gitroom/backend/services/auth/auth.service"; import {ApiModule} from "@gitroom/backend/api/api.module"; -import {AuthMiddleware} from "@gitroom/backend/services/auth/auth.middleware"; +import {APP_GUARD} from "@nestjs/core"; +import {PoliciesGuard} from "@gitroom/backend/services/auth/permissions/permissions.guard"; +@Global() @Module({ imports: [DatabaseModule, RedisModule, ApiModule], controllers: [], - providers: [AuthService, AuthMiddleware], + providers: [{ + provide: APP_GUARD, + useClass: PoliciesGuard + }], get exports() { - return [...this.imports, ...this.providers]; + return [...this.imports]; } }) export class AppModule {} diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 47fc8c23..e2402569 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -2,6 +2,7 @@ import cookieParser from 'cookie-parser'; import {Logger, ValidationPipe} from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import {SubscriptionExceptionFilter} from "@gitroom/backend/services/auth/permissions/subscription.exception"; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -17,6 +18,7 @@ async function bootstrap() { })); app.use(cookieParser()); + app.useGlobalFilters(new SubscriptionExceptionFilter()); const port = process.env.PORT || 3000; await app.listen(port); diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index d2bf7d8d..413502ba 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -1,12 +1,16 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; -import * as console from "console"; import {AuthService} from "@gitroom/helpers/auth/auth.service"; import {User} from '@prisma/client'; +import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; @Injectable() export class AuthMiddleware implements NestMiddleware { - use(req: Request, res: Response, next: NextFunction) { + constructor( + private _organizationService: OrganizationService, + ) { + } + async use(req: Request, res: Response, next: NextFunction) { const auth = req.headers.auth || req.cookies.auth; if (!auth) { throw new Error('Unauthorized'); @@ -18,9 +22,14 @@ export class AuthMiddleware implements NestMiddleware { } delete user.password; + const organization = await this._organizationService.getFirstOrgByUserId(user.id); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error req.user = user; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + req.org = organization; } catch (err) { throw new Error('Unauthorized'); diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index 77a4f521..e58ec775 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -6,7 +6,6 @@ import {UsersService} from "@gitroom/nestjs-libraries/database/prisma/users/user import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; import {AuthService as AuthChecker} from "@gitroom/helpers/auth/auth.service"; import {ProvidersFactory} from "@gitroom/backend/services/auth/providers/providers.factory"; -import * as console from "console"; @Injectable() export class AuthService { diff --git a/apps/backend/src/services/auth/permissions/permissions.ability.ts b/apps/backend/src/services/auth/permissions/permissions.ability.ts new file mode 100644 index 00000000..b2c1a3cf --- /dev/null +++ b/apps/backend/src/services/auth/permissions/permissions.ability.ts @@ -0,0 +1,6 @@ +import {SetMetadata} from "@nestjs/common"; +import {AuthorizationActions, Sections} from "@gitroom/backend/services/auth/permissions/permissions.service"; + +export const CHECK_POLICIES_KEY = 'check_policy'; +export type AbilityPolicy = [AuthorizationActions, Sections]; +export const CheckPolicies = (...handlers: AbilityPolicy[]) => SetMetadata(CHECK_POLICIES_KEY, handlers); diff --git a/apps/backend/src/services/auth/permissions/permissions.guard.ts b/apps/backend/src/services/auth/permissions/permissions.guard.ts new file mode 100644 index 00000000..4855a907 --- /dev/null +++ b/apps/backend/src/services/auth/permissions/permissions.guard.ts @@ -0,0 +1,55 @@ +import {CanActivate, ExecutionContext, Injectable} from "@nestjs/common"; +import {Reflector} from "@nestjs/core"; +import {AppAbility, PermissionsService} from "@gitroom/backend/services/auth/permissions/permissions.service"; +import {AbilityPolicy, CHECK_POLICIES_KEY} from "@gitroom/backend/services/auth/permissions/permissions.ability"; +import {Organization} from "@prisma/client"; +import {SubscriptionException} from "@gitroom/backend/services/auth/permissions/subscription.exception"; +import {Request} from "express"; + + +@Injectable() +export class PoliciesGuard implements CanActivate { + constructor( + private _reflector: Reflector, + private _authorizationService: PermissionsService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request: Request = context.switchToHttp().getRequest(); + if (request.path.indexOf('/auth') > -1 || request.path.indexOf('/stripe') > -1) { + return true; + } + + const policyHandlers = + this._reflector.get( + CHECK_POLICIES_KEY, + context.getHandler(), + ) || []; + + if (!policyHandlers || !policyHandlers.length) { + return true; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const { org } : {org: Organization} = request; + const ability = await this._authorizationService.check(org.id); + + const item = policyHandlers.find((handler) => + !this.execPolicyHandler(handler, ability), + ); + + if (item) { + throw new SubscriptionException({ + section: item[1], + action: item[0] + }); + } + + return true; + } + + private execPolicyHandler(handler: AbilityPolicy, ability: AppAbility) { + return ability.can(handler[0], handler[1]); + } +} diff --git a/apps/backend/src/services/auth/permissions/permissions.service.ts b/apps/backend/src/services/auth/permissions/permissions.service.ts new file mode 100644 index 00000000..9b6fc934 --- /dev/null +++ b/apps/backend/src/services/auth/permissions/permissions.service.ts @@ -0,0 +1,48 @@ +import {Ability, AbilityBuilder, AbilityClass} from "@casl/ability"; +import {Injectable} from "@nestjs/common"; +import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing"; +import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service"; + +export enum Sections { + FRIENDS = 'friends', + CROSSPOSTING = 'crossposting', + AI = 'ai', + INTEGRATIONS = 'integrations', + TOTALPOSTS = 'totalPosts', + MEDIAS = 'medias', + INFLUENCERS = 'influencers', +} + +export enum AuthorizationActions { + Create = 'create', + Read = 'read', + Update = 'update', + Delete = 'delete', +} + +export type AppAbility = Ability<[AuthorizationActions, Sections]>; + +@Injectable() +export class PermissionsService { + constructor( + private _subscriptionService: SubscriptionService, + ) { + } + async getPackageOptions(orgId: string) { + const subscription = await this._subscriptionService.getSubscriptionByOrganizationId(orgId); + return pricing[subscription?.subscriptionTier || !process.env.PAYMENT_PUBLIC_KEY ? 'PRO' : 'FREE']; + } + + async check(orgId: string) { + const { can, build } = new AbilityBuilder>(Ability as AbilityClass); + + // const options = await this.getPackageOptions(orgId); + + return build({ + detectSubjectType: (item) => + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + item.constructor + }); + } +} diff --git a/apps/backend/src/services/auth/permissions/subscription.exception.ts b/apps/backend/src/services/auth/permissions/subscription.exception.ts new file mode 100644 index 00000000..6734dd06 --- /dev/null +++ b/apps/backend/src/services/auth/permissions/subscription.exception.ts @@ -0,0 +1,44 @@ +import {ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus} from "@nestjs/common"; +import {AuthorizationActions, Sections} from "@gitroom/backend/services/auth/permissions/permissions.service"; + +export class SubscriptionException extends HttpException { + constructor(message: { + section: Sections, + action: AuthorizationActions + }) { + super(message, HttpStatus.PAYMENT_REQUIRED); + } +} + +@Catch(SubscriptionException) +export class SubscriptionExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = exception.getStatus(); + const error: {section: Sections, action: AuthorizationActions} = exception.getResponse() as any; + + const message = getErrorMessage(error); + + response.status(status).json({ + statusCode: status, + message, + url: process.env.FRONTEND_URL + '/billing', + }); + } +} + +const getErrorMessage = (error: {section: Sections, action: AuthorizationActions}) => { + switch (error.section) { + case Sections.AI: + switch (error.action) { + default: + return 'You have reached the maximum number of FAQ\'s for your subscription. Please upgrade your subscription to add more FAQ\'s.'; + } + case Sections.CROSSPOSTING: + switch (error.action) { + default: + return 'You have reached the maximum number of categories for your subscription. Please upgrade your subscription to add more categories.'; + } + } +} diff --git a/apps/commands/src/tasks/check.stars.ts b/apps/commands/src/tasks/check.stars.ts index d1c65a5c..1845811c 100644 --- a/apps/commands/src/tasks/check.stars.ts +++ b/apps/commands/src/tasks/check.stars.ts @@ -1,7 +1,6 @@ import {Command, Positional} from 'nestjs-command'; import { Injectable } from '@nestjs/common'; import {BullMqClient} from "@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client"; -import * as console from "console"; @Injectable() export class CheckStars { @@ -24,4 +23,29 @@ export class CheckStars { this._workerServiceProducer.emit('check_stars', {payload: {login}}).subscribe(); return true; } + + @Command({ + command: 'sync:all_stars ', + describe: 'Sync all stars for a login', + }) + async syncAllStars( + @Positional({ + name: 'login', + describe: 'login {owner}/{repo}', + type: 'string' + }) + login: string, + ) { + this._workerServiceProducer.emit('sync_all_stars', {payload: {login}}).subscribe(); + return true; + } + + @Command({ + command: 'sync:trending', + describe: 'Sync trending', + }) + async syncTrending() { + this._workerServiceProducer.emit('sync_trending', {}).subscribe(); + return true; + } } \ No newline at end of file diff --git a/apps/cron/src/tasks/check.stars.ts b/apps/cron/src/tasks/check.stars.ts index 8018898b..43603036 100644 --- a/apps/cron/src/tasks/check.stars.ts +++ b/apps/cron/src/tasks/check.stars.ts @@ -1,14 +1,13 @@ import { Injectable } from '@nestjs/common'; import {Cron} from '@nestjs/schedule'; import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; -import {BullMqClient} from "@gitroom/nestjs-libraries/bullmq-transport/bullmq-client"; -import {WorkerServiceProducer} from "@gitroom/nestjs-libraries/bullmq-transport/bullmq-register"; +import {BullMqClient} from "@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client"; @Injectable() export class CheckStars { constructor( private _starsService: StarsService, - @WorkerServiceProducer() private _workerServiceProducer: BullMqClient + private _workerServiceProducer: BullMqClient ) { } @Cron('0 0 * * *') diff --git a/apps/frontend/project.json b/apps/frontend/project.json index ce841169..43ce2c57 100644 --- a/apps/frontend/project.json +++ b/apps/frontend/project.json @@ -10,7 +10,7 @@ "defaultConfiguration": "production", "options": { "outputPath": "dist/apps/frontend", - "postcssConfig": "apps/{your app here}/postcss.config.js" + "postcssConfig": "apps/frontend/postcss.config.js" }, "configurations": { "development": { diff --git a/apps/frontend/public/form/checked.svg b/apps/frontend/public/form/checked.svg new file mode 100644 index 00000000..a0e02555 --- /dev/null +++ b/apps/frontend/public/form/checked.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/frontend/public/icons/github.svg b/apps/frontend/public/icons/github.svg new file mode 100644 index 00000000..052aed73 --- /dev/null +++ b/apps/frontend/public/icons/github.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/frontend/public/icons/star-circle.svg b/apps/frontend/public/icons/star-circle.svg new file mode 100644 index 00000000..e1436b0e --- /dev/null +++ b/apps/frontend/public/icons/star-circle.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/icons/trending.svg b/apps/frontend/public/icons/trending.svg new file mode 100644 index 00000000..568ae455 --- /dev/null +++ b/apps/frontend/public/icons/trending.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/frontend/src/app/(site)/analytics/page.tsx b/apps/frontend/src/app/(site)/analytics/page.tsx index fe088ce5..5fb26a1b 100644 --- a/apps/frontend/src/app/(site)/analytics/page.tsx +++ b/apps/frontend/src/app/(site)/analytics/page.tsx @@ -1,5 +1,15 @@ +import {AnalyticsComponent} from "@gitroom/frontend/components/analytics/analytics.component"; +import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; + export default async function Index() { + const analytics = await (await internalFetch('/analytics')).json(); + const trending = await (await internalFetch('/analytics/trending')).json(); + const stars = await (await internalFetch('/analytics/stars', { + body: JSON.stringify({page: 1}), + method: 'POST' + })).json(); + return ( - <>asd + ); } diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx new file mode 100644 index 00000000..9884c9bb --- /dev/null +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx @@ -0,0 +1,11 @@ +import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; +import {redirect} from "next/navigation"; + +export default async function Page({params: {provider}, searchParams}: {params: {provider: string}, searchParams: object}) { + await internalFetch(`/integrations/social/${provider}/connect`, { + method: 'POST', + body: JSON.stringify(searchParams) + }); + + return redirect(`/launches?added=${provider}`); +} \ No newline at end of file diff --git a/apps/frontend/src/app/(site)/schedule/page.tsx b/apps/frontend/src/app/(site)/launches/page.tsx similarity index 100% rename from apps/frontend/src/app/(site)/schedule/page.tsx rename to apps/frontend/src/app/(site)/launches/page.tsx diff --git a/apps/frontend/src/app/(site)/settings/page.tsx b/apps/frontend/src/app/(site)/settings/page.tsx new file mode 100644 index 00000000..16f1fb8a --- /dev/null +++ b/apps/frontend/src/app/(site)/settings/page.tsx @@ -0,0 +1,23 @@ +import {SettingsComponent} from "@gitroom/frontend/components/settings/settings.component"; +import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; +import {redirect} from "next/navigation"; +import {RedirectType} from "next/dist/client/components/redirect"; + +export default async function Index({searchParams}: {searchParams: {code: string}}) { + if (searchParams.code) { + await internalFetch('/settings/github', { + method: 'POST', + body: JSON.stringify({code: searchParams.code}) + }); + + return redirect('/settings', RedirectType.replace); + } + + const {github} = await (await internalFetch('/settings/github')).json(); + const emptyOnes = github.find((p: {login: string}) => !p.login); + const {organizations} = emptyOnes ? await (await internalFetch(`/settings/organizations/${emptyOnes.id}`)).json() : {organizations: []}; + + return ( + + ); +} diff --git a/apps/frontend/src/app/global.css b/apps/frontend/src/app/global.css index b5c61c95..2b0ec37a 100644 --- a/apps/frontend/src/app/global.css +++ b/apps/frontend/src/app/global.css @@ -1,3 +1,58 @@ @tailwind base; @tailwind components; @tailwind utilities; + +/*body, html {*/ +/* overflow-x: hidden;*/ +/*}*/ +.box { + position: relative; + padding: 8px 24px; +} +.box span { + position: relative; + z-index: 2; +} +.box:after { + border-radius: 50px; + width: 100%; + height: 100%; + left: 0; + top: 0; + content: ""; + position: absolute; + background: white; + opacity: 0; + z-index: 1; + transition: all 0.3s ease-in-out; +} + +.showbox { + color: black; +} +.showbox:after { + opacity: 1; + background: white; + transition: all 0.3s ease-in-out; +} +.table1 { + width: 100%; + border-collapse: collapse; +} + +.table1 thead { + background-color: #111423; + height: 44px; + font-size: 12px; + border-bottom: 1px solid #28344F; +} +.table1 thead th, .table1 tbody td { + text-align: left; + padding: 14px 24px; +} + +.table1 tbody td { + padding: 16px 24px; + font-family: Inter; + font-size: 14px; +} \ No newline at end of file diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 1d8df642..7668f2c4 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -1,10 +1,11 @@ import LayoutContext from "@gitroom/frontend/components/layout/layout.context"; import {ReactNode} from "react"; - +import {Chakra_Petch} from "next/font/google"; +const chakra = Chakra_Petch({weight: '400', subsets: ['latin']}) export default async function AppLayout({children}: {children: ReactNode}) { return ( - + {children} diff --git a/apps/frontend/src/components/analytics/analytics.component.tsx b/apps/frontend/src/components/analytics/analytics.component.tsx new file mode 100644 index 00000000..f7e471c3 --- /dev/null +++ b/apps/frontend/src/components/analytics/analytics.component.tsx @@ -0,0 +1,105 @@ +import {StarsAndForks} from "@gitroom/frontend/components/analytics/stars.and.forks"; +import {FC} from "react"; +import {StarsAndForksInterface} from "@gitroom/frontend/components/analytics/stars.and.forks.interface"; +import {StarsTableComponent} from "@gitroom/frontend/components/analytics/stars.table.component"; + +export const AnalyticsComponent: FC = (props) => { + return ( +
+
+ +
+

Stars per day

+
+ +
+
+
+
+

News Feed

+
+
+ Global +
+
+ My Feed +
+
+
+
+
+ +
+
+
Nevo David
+
05/06/2024
+
+
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
+
+
See Tweet
+
+
+
+
+
+ +
+
+
Nevo David
+
05/06/2024
+
+
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
+
+
See Tweet
+
+
+
+
+
+ +
+
+
Nevo David
+
05/06/2024
+
+
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
+
+
See Tweet
+
+
+
+
+
+ +
+
+
Nevo David
+
05/06/2024
+
+
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
+
+
See Tweet
+
+
+
+
+
+ +
+
+
Nevo David
+
05/06/2024
+
+
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
+
+
See Tweet
+
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/analytics/chart.tsx b/apps/frontend/src/components/analytics/chart.tsx new file mode 100644 index 00000000..70132651 --- /dev/null +++ b/apps/frontend/src/components/analytics/chart.tsx @@ -0,0 +1,64 @@ +"use client"; +import {FC, useEffect, useMemo, useRef} from "react"; +import DrawChart from 'chart.js/auto'; +import {StarsList} from "@gitroom/frontend/components/analytics/stars.and.forks.interface"; +import dayjs from "dayjs"; + +export const Chart: FC<{list: StarsList[]}> = (props) => { + const {list} = props; + const ref = useRef(null); + const chart = useRef(null); + useEffect(() => { + const gradient = ref.current.getContext('2d').createLinearGradient(0, 0, 0, ref.current.height); + gradient.addColorStop(0, 'rgba(114, 118, 137, 1)'); // Start color with some transparency + gradient.addColorStop(1, 'rgb(9, 11, 19, 1)'); + chart.current = new DrawChart( + ref.current!, + { + type: 'line', + options: { + maintainAspectRatio: false, + responsive: true, + layout: { + padding: { + left: 0, + right: 0, + top: 0, + bottom: 0 + } + }, + scales: { + y: { + beginAtZero: true, + display: false + }, + x: { + display: false + } + }, + plugins: { + legend: { + display: false + }, + } + }, + data: { + labels: list.map(row => dayjs(row.date).format('DD/MM/YYYY')), + datasets: [ + { + borderColor: '#fff', + label: 'Stars by date', + backgroundColor: gradient, + fill: true, + data: list.map(row => row.totalStars) + } + ] + } + } + ); + return () => { + chart?.current?.destroy(); + } + }, []); + return +} \ No newline at end of file diff --git a/apps/frontend/src/components/analytics/stars.and.forks.interface.ts b/apps/frontend/src/components/analytics/stars.and.forks.interface.ts new file mode 100644 index 00000000..ad380707 --- /dev/null +++ b/apps/frontend/src/components/analytics/stars.and.forks.interface.ts @@ -0,0 +1,23 @@ +export interface StarsList { + totalStars: number; + date: string; +} +export interface Stars { + id: string, + stars: number, + totalStars: number, + login: string, + date: string, + +} +export interface StarsAndForksInterface { + list: Array<{ + login: string; + stars: StarsList[] + }>; + trending: { + last: string; + predictions: string; + }; + stars: Stars[]; +} \ No newline at end of file diff --git a/apps/frontend/src/components/analytics/stars.and.forks.tsx b/apps/frontend/src/components/analytics/stars.and.forks.tsx new file mode 100644 index 00000000..e893153e --- /dev/null +++ b/apps/frontend/src/components/analytics/stars.and.forks.tsx @@ -0,0 +1,66 @@ +import {FC} from "react"; +import {StarsAndForksInterface} from "@gitroom/frontend/components/analytics/stars.and.forks.interface"; +import {Chart} from "@gitroom/frontend/components/analytics/chart"; +import Image from "next/image"; +import {UtcToLocalDateRender} from "../../../../../libraries/react-shared-libraries/src/helpers/utc.date.render"; +import clsx from "clsx"; + +export const StarsAndForks: FC = (props) => { + const {list} = props; + console.log(list); + return ( + <> + {list.map(item => ( +
+ {[1,2].map(p => ( +
+
+
+ Stars +
+
+ {item.login.split('/')[1].split('').map(((char, index) => index === 0 ? char.toUpperCase() : char)).join('')} {p === 1 ? 'Stars' : 'Forks'} +
+
+
+
+ {item.stars.length ? :
Processing stars...
} +
+
+
+ {item?.stars[item.stars.length - 1]?.totalStars} +
+
+ ))} +
+ ))} +
+ {[0, 1].map( p => ( +
+
+
+ Trending +
+
+ {p === 0 ? 'Last Github Trending' : 'Next Predicted GitHub Trending'} +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
))} +
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/analytics/stars.table.component.tsx b/apps/frontend/src/components/analytics/stars.table.component.tsx new file mode 100644 index 00000000..36c5e23a --- /dev/null +++ b/apps/frontend/src/components/analytics/stars.table.component.tsx @@ -0,0 +1,30 @@ +import {FC} from "react"; +import {Stars} from "@gitroom/frontend/components/analytics/stars.and.forks.interface"; +import {UtcToLocalDateRender} from "../../../../../libraries/react-shared-libraries/src/helpers/utc.date.render"; + +export const StarsTableComponent: FC<{stars: Stars[]}> = (props) => { + const {stars} = props; + return ( + + + + + + + + + + + + {stars.map(p => ( + + + + + + + ))} + +
RepositoryDateTotalStarsMedia
{p.login}{p.totalStars}{p.stars}Media
+ ) +} \ No newline at end of file diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx index 92dfe918..0d969b51 100644 --- a/apps/frontend/src/components/auth/register.tsx +++ b/apps/frontend/src/components/auth/register.tsx @@ -18,8 +18,8 @@ export function Register() { const fetchData = useFetch(); - const onSubmit: SubmitHandler = (data) => { - fetchData('/auth/register', { + const onSubmit: SubmitHandler = async (data) => { + await fetchData('/auth/register', { method: 'POST', body: JSON.stringify({...data, provider: 'LOCAL'}) }); diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx index 262097ca..4310ae73 100644 --- a/apps/frontend/src/components/layout/layout.context.tsx +++ b/apps/frontend/src/components/layout/layout.context.tsx @@ -3,9 +3,16 @@ import {ReactNode, useCallback} from "react"; import {FetchWrapperComponent} from "@gitroom/helpers/utils/custom.fetch"; -export default async function LayoutContext({children}: {children: ReactNode}) { +export default async function LayoutContext(params: {children: ReactNode}) { + if (params?.children) { + // eslint-disable-next-line react/no-children-prop + return + } + + return <> +} +function LayoutContextInner(params: {children: ReactNode}) { const afterRequest = useCallback(async (url: string, options: RequestInit, response: Response) => { - console.log(response?.headers.get('cookie')); if (response?.headers?.get('reload')) { window.location.reload(); } @@ -16,7 +23,7 @@ export default async function LayoutContext({children}: {children: ReactNode}) { baseUrl={process.env.NEXT_PUBLIC_BACKEND_URL!} afterRequest={afterRequest} > - {children} + {params?.children || <>} ) } \ No newline at end of file diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index a8a65241..55a6f16c 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -1,24 +1,30 @@ import {ReactNode} from "react"; -import {LeftMenu} from "@gitroom/frontend/components/layout/left.menu"; import {Title} from "@gitroom/frontend/components/layout/title"; import {headers} from "next/headers"; import {ContextWrapper} from "@gitroom/frontend/components/layout/user.context"; +import {NotificationComponent} from "@gitroom/frontend/components/notifications/notification.component"; +import {TopMenu} from "@gitroom/frontend/components/layout/top.menu"; export const LayoutSettings = ({children}: {children: ReactNode}) => { const user = JSON.parse(headers().get('user')!); return ( -
-
-
- Logo +
+
+
+ Gitroom +
+ +
+
-
-
+
- {children} + <div className="flex flex-1 flex-col"> + {children} + </div> </div> </div> </div> diff --git a/apps/frontend/src/components/layout/title.tsx b/apps/frontend/src/components/layout/title.tsx index bd34b82e..2cf2aaa3 100644 --- a/apps/frontend/src/components/layout/title.tsx +++ b/apps/frontend/src/components/layout/title.tsx @@ -2,8 +2,7 @@ import {usePathname} from "next/navigation"; import {useMemo} from "react"; -import {menuItems} from "@gitroom/frontend/components/layout/left.menu"; -import {useUser} from "@gitroom/frontend/components/layout/user.context"; +import {menuItems} from "@gitroom/frontend/components/layout/top.menu"; export const Title = () => { const path = usePathname(); @@ -11,13 +10,9 @@ export const Title = () => { return menuItems.find(item => item.path === path)?.name; }, [path]); - const user = useUser(); - return ( <div className="flex"> - <h1 className="text-2xl mb-5 flex-1">{currentTitle}</h1> - <div>bell</div> - <div>{user?.email}</div> + <h1 className="text-[24px] mb-5 flex-1">{currentTitle}</h1> </div> ); } \ No newline at end of file diff --git a/apps/frontend/src/components/layout/left.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx similarity index 69% rename from apps/frontend/src/components/layout/left.menu.tsx rename to apps/frontend/src/components/layout/top.menu.tsx index 3d095a10..75934646 100644 --- a/apps/frontend/src/components/layout/left.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -12,9 +12,9 @@ export const menuItems = [ path: '/analytics', }, { - name: 'Schedule', - icon: 'schedule', - path: '/schedule', + name: 'Launches', + icon: 'launches', + path: '/launches', }, { name: 'Media', @@ -33,22 +33,19 @@ export const menuItems = [ }, ]; -export const LeftMenu: FC = () => { +export const TopMenu: FC = () => { const path = usePathname(); return ( <div className="flex flex-col h-full"> - <ul className="gap-5 flex flex-col flex-1"> + <ul className="gap-5 flex flex-1 items-center text-[18px]"> {menuItems.map((item, index) => ( <li key={item.name}> - <Link href={item.path} className={clsx("flex gap-2 items-center", menuItems.map(p => p.path).indexOf(path) === index && 'font-bold')}> - {item.name} + <Link href={item.path} className={clsx("flex gap-2 items-center box", menuItems.map(p => p.path).indexOf(path) === index ? 'text-primary showbox' : 'text-gray')}> + <span>{item.name}</span> </Link> </li> ))} </ul> - <div> - <a href="/auth/logout">Logout</a> - </div> </div> ); } \ No newline at end of file diff --git a/apps/frontend/src/components/notifications/notification.component.tsx b/apps/frontend/src/components/notifications/notification.component.tsx new file mode 100644 index 00000000..4de8d430 --- /dev/null +++ b/apps/frontend/src/components/notifications/notification.component.tsx @@ -0,0 +1,18 @@ +"use client"; + +import {NotificationBell, NovuProvider, PopoverNotificationCenter} from "@novu/notification-center"; +import {useUser} from "@gitroom/frontend/components/layout/user.context"; + +export const NotificationComponent = () => { + const user = useUser(); + return ( + <NovuProvider + subscriberId={user?.id} + applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER!} + > + <PopoverNotificationCenter colorScheme="dark"> + {({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />} + </PopoverNotificationCenter> + </NovuProvider> + ) +} \ No newline at end of file diff --git a/apps/frontend/src/components/settings/github.component.tsx b/apps/frontend/src/components/settings/github.component.tsx new file mode 100644 index 00000000..8b74ec28 --- /dev/null +++ b/apps/frontend/src/components/settings/github.component.tsx @@ -0,0 +1,122 @@ +"use client"; +import Image from "next/image"; +import {Button} from "@gitroom/react/form/button"; +import {FC, useCallback, useEffect, useState} from "react"; +import {useFetch} from "@gitroom/helpers/utils/custom.fetch"; + +const ConnectedComponent: FC<{id: string, login: string}> = (props) => { + const {id, login} = props; + return ( + <div className="my-[16px] mt-[16px] h-[90px] bg-sixth border-fifth border rounded-[4px] p-[24px]"> + <div className="flex items-center gap-[8px] font-[Inter]"> + <div><Image src="/icons/github.svg" alt="GitHub" width={40} height={40}/></div> + <div className="flex-1"><strong>Connected:</strong> {login}</div> + <Button>Disconnect</Button> + </div> + </div> + ); +} + +const RepositoryComponent: FC<{id: string, login?: string, setRepo: (name: string) => void}> = (props) => { + const {setRepo, login, id} = props; + const [repositories, setRepositories] = useState<Array<{id: string, name: string}>>([]); + const fetch = useFetch(); + + const loadRepositories = useCallback(async () => { + const {repositories: repolist} = await (await fetch(`/settings/organizations/${id}/${login}`)).json(); + setRepositories(repolist); + }, [login, id]); + + useEffect(() => { + setRepositories([]); + if (!login) { + return ; + } + + loadRepositories(); + }, [login]); + if (!login || !repositories.length) { + return <></>; + } + + return ( + <select className="border border-fifth bg-transparent h-[40px]" onChange={(e) => setRepo(e.target.value)}> + <option value="">Choose a repository</option> + {repositories.map(o => (<option key={o.id} value={o.name}>{o.name}</option>))} + </select>) +} + +const ConnectComponent: FC<{ setConnected: (name: string) => void, id: string, login: string, organizations: Array<{ id: string, login: string }> }> = (props) => { + const {id, setConnected} = props; + const [repo, setRepo] = useState<undefined | string>(); + const [select, setSelect] = useState<undefined | string>(); + const fetch = useFetch(); + + const completeConnection = useCallback(async () => { + setConnected(`${select}/${repo}`); + await (await fetch(`/settings/organizations/${id}`, { + method: 'POST', + body: JSON.stringify({login: `${select}/${repo}`}) + })).json(); + }, [repo, select]); + + return ( + <div className="my-[16px] mt-[16px] h-[90px] bg-sixth border-fifth border rounded-[4px] p-[24px]"> + <div className="flex items-center gap-[8px] font-[Inter]"> + <div><Image src="/icons/github.svg" alt="GitHub" width={40} height={40}/></div> + <div className="flex-1">Connect your repository</div> + <select className="border border-fifth bg-transparent h-[40px]" value={select} onChange={(e) => setSelect(e.target.value)}> + <option value="">Choose an organization</option> + {props.organizations.map(o => ( + <option key={o.id} value={o.login}>{o.login}</option> + ))} + </select> + <RepositoryComponent id={id} login={select} setRepo={setRepo} /> + {!!repo && <Button onClick={completeConnection}>Connect</Button>} + </div> + </div> + ); +} + +export const GithubComponent: FC<{ organizations: Array<{ login: string, id: string }>, github: Array<{ id: string, login: string }> }> = (props) => { + const {github, organizations} = props; + const [githubState, setGithubState] = useState(github); + const fetch = useFetch(); + const connect = useCallback(async () => { + const {url} = await (await fetch('/settings/github/url')).json(); + window.location.href = url; + }, []); + + const setConnected = useCallback((g: {id: string, login: string}) => (name: string) => { + setGithubState((gitlibs) => { + return gitlibs.map((git, index) => { + if (git.id === g.id) { + return {id: g.id, login: name}; + } + return g; + }) + }); + }, []); + + return ( + <> + {githubState.map(g => ( + <> + {!g.login ? ( + <ConnectComponent setConnected={setConnected(g)} organizations={organizations} {...g} /> + ): ( + <ConnectedComponent {...g} /> + )} + </> + ))} + {githubState.filter(f => !f.login).length === 0 && (<div className="my-[16px] mt-[16px] h-[90px] bg-sixth border-fifth border rounded-[4px] p-[24px]"> + <div className="flex items-center gap-[8px] font-[Inter]"> + <div><Image src="/icons/github.svg" alt="GitHub" width={40} height={40}/></div> + <div className="flex-1">Connect your repository</div> + <Button onClick={connect}>Connect</Button> + </div> + </div> + )} + </> + ) +} \ No newline at end of file diff --git a/apps/frontend/src/components/settings/settings.component.tsx b/apps/frontend/src/components/settings/settings.component.tsx new file mode 100644 index 00000000..1d60f15a --- /dev/null +++ b/apps/frontend/src/components/settings/settings.component.tsx @@ -0,0 +1,38 @@ +import {Button} from "@gitroom/react/form/button"; +import {Checkbox} from "@gitroom/react/form/checkbox"; +import {GithubComponent} from "@gitroom/frontend/components/settings/github.component"; +import {FC} from "react"; + +export const SettingsComponent: FC<{organizations: Array<{login: string, id: string}>, github: Array<{id: string, login: string}>}> = (props) => { + const {github, organizations} = props; + return ( + <div className="flex flex-col gap-[68px]"> + <div className="flex flex-col"> + <h3 className="text-[20px]">Your Git Repository</h3> + <div className="text-[#AAA] mt-[4px]">Connect your GitHub repository to receive updates and analytics</div> + <GithubComponent github={github} organizations={organizations} /> + <div className="flex gap-[5px]"> + <div><Checkbox checked={true} /></div> + <div>Show news with everybody in Gitroom</div> + </div> + </div> + <div className="flex flex-col"> + <h2 className="text-[24px] mb-[24px]">Team Members</h2> + <h3 className="text-[20px]">Account Managers</h3> + <div className="text-[#AAA] mt-[4px]">Invite your assistant or team member to manage your Gitroom account</div> + <div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[24px]"> + <div className="flex flex-col gap-[16px]"> + <div className="flex justify-between"> + <div>Nevo David</div> + <div>Administrator</div> + <div>Remove</div> + </div> + </div> + <div> + <Button>Add another member</Button> + </div> + </div> + </div> + </div> + ); +} \ No newline at end of file diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index 385e75c1..6d4c00c8 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -3,6 +3,11 @@ const { join } = require('path'); module.exports = { content: [ + ...createGlobPatternsForDependencies(__dirname + '../../../libraries/react-shared-libraries'), + join( + __dirname + '../../../libraries/react-shared-libraries', + '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' + ), join( __dirname, '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' @@ -12,8 +17,13 @@ module.exports = { theme: { extend: { colors: { - primary: '#090b13', - secondary: '#0b101b', + primary: '#000', + secondary: '#090B13', + third: '#080B13', + forth: '#262373', + fifth: '#172034', + sixth: '#0B101B', + gray: '#8C8C8C', } }, }, diff --git a/apps/workers/src/app/app.module.ts b/apps/workers/src/app/app.module.ts index 9d7ac4d3..f0ba9863 100644 --- a/apps/workers/src/app/app.module.ts +++ b/apps/workers/src/app/app.module.ts @@ -5,12 +5,13 @@ import {RedisModule} from "@gitroom/nestjs-libraries/redis/redis.module"; import {DatabaseModule} from "@gitroom/nestjs-libraries/database/prisma/database.module"; import {BullMqModule} from "@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module"; import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service"; +import {TrendingService} from "@gitroom/nestjs-libraries/services/trending.service"; @Module({ imports: [RedisModule, DatabaseModule, BullMqModule.forRoot({ connection: ioRedis })], controllers: [StarsController], - providers: [], + providers: [TrendingService], }) export class AppModule {} diff --git a/apps/workers/src/app/stars.controller.ts b/apps/workers/src/app/stars.controller.ts index bfbe0c43..2814bddd 100644 --- a/apps/workers/src/app/stars.controller.ts +++ b/apps/workers/src/app/stars.controller.ts @@ -1,17 +1,51 @@ import {Controller} from '@nestjs/common'; import {EventPattern, Transport} from '@nestjs/microservices'; import { JSDOM } from "jsdom"; +import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; +import {TrendingService} from "@gitroom/nestjs-libraries/services/trending.service"; @Controller() export class StarsController { + constructor( + private _starsService: StarsService, + private _trendingService: TrendingService + ) { + } @EventPattern('check_stars', Transport.REDIS) - async handleData(data: {id: string, login: string}) { + async checkStars(data: {login: string}) { + // no to be effected by the limit, we scrape the HTML instead of using the API const loadedHtml = await (await fetch(`https://github.com/${data.login}`)).text(); const dom = new JSDOM(loadedHtml); const totalStars = +( dom.window.document.querySelector('#repo-stars-counter-star')?.getAttribute('title')?.replace(/,/g, '') ) || 0; - console.log(totalStars); + const lastStarsValue = await this._starsService.getLastStarsByLogin(data.login); + const totalNewsStars = totalStars - (lastStarsValue?.totalStars || 0); + + // if there is no stars in the database, we need to sync the stars + if (!lastStarsValue?.totalStars) { + return; + } + + // if there is stars in the database, sync the new stars + if (totalNewsStars > 0) { + return this._starsService.createStars(data.login, totalNewsStars, totalStars, new Date()); + } + } + + @EventPattern('sync_all_stars', Transport.REDIS, {concurrency: 1}) + async syncAllStars(data: {login: string}) { + // if there is a sync in progress, it's better not to touch it + if ((await this._starsService.getStarsByLogin(data.login)).length) { + return; + } + + await this._starsService.sync(data.login); + } + + @EventPattern('sync_trending', Transport.REDIS, {concurrency: 1}) + async syncTrending() { + return this._trendingService.syncTrending(); } } diff --git a/libraries/helpers/src/utils/internal.fetch.ts b/libraries/helpers/src/utils/internal.fetch.ts new file mode 100644 index 00000000..c948ec02 --- /dev/null +++ b/libraries/helpers/src/utils/internal.fetch.ts @@ -0,0 +1,4 @@ +import {customFetch} from "./custom.fetch.func"; +import {cookies} from "next/headers"; + +export const internalFetch = (url: string, options: RequestInit = {}) => customFetch({baseUrl: process.env.BACKEND_INTERNAL_URL!}, cookies()?.get('auth')?.value!)(url, options); \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts b/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts index 90e969e4..4573dfd2 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts @@ -52,6 +52,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }, { ...this.options, + ...handler?.extras }, ); this.workers.set(pattern, worker); diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts index 96ee851b..6c8bacf6 100644 --- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts +++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts @@ -6,6 +6,11 @@ import {UsersService} from "@gitroom/nestjs-libraries/database/prisma/users/user import {UsersRepository} from "@gitroom/nestjs-libraries/database/prisma/users/users.repository"; import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; import {StarsRepository} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.repository"; +import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service"; +import {SubscriptionRepository} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository"; +import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service"; +import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service"; +import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository"; @Global() @Module({ @@ -19,7 +24,12 @@ import {StarsRepository} from "@gitroom/nestjs-libraries/database/prisma/stars/s OrganizationService, OrganizationRepository, StarsService, - StarsRepository + StarsRepository, + SubscriptionService, + SubscriptionRepository, + NotificationService, + IntegrationService, + IntegrationRepository ], get exports() { return this.providers; diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts new file mode 100644 index 00000000..691627b5 --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -0,0 +1,25 @@ +import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service"; +import {Injectable} from "@nestjs/common"; + +@Injectable() +export class IntegrationRepository { + constructor( + private _integration: PrismaRepository<'integration'> + ) { + } + + createIntegration(org: string, name: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn = 999999999) { + return this._integration.model.integration.create({ + data: { + type: type as any, + name, + providerIdentifier: provider, + token, + refreshToken, + ...expiresIn ? {tokenExpiration: new Date(Date.now() + expiresIn * 1000)} :{}, + internalId, + organizationId: org, + } + }) + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts new file mode 100644 index 00000000..140a10ec --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -0,0 +1,13 @@ +import {Injectable} from "@nestjs/common"; +import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository"; + +@Injectable() +export class IntegrationService { + constructor( + private _integrationRepository: IntegrationRepository, + ) { + } + createIntegration(org: string, name: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn?: number) { + return this._integrationRepository.createIntegration(org, name, type, internalId, provider, token, refreshToken, expiresIn); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index 54cd84f9..a9966a5d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -11,6 +11,26 @@ export class OrganizationRepository { ) { } + async getFirstOrgByUserId(userId: string) { + return this._organization.model.organization.findFirst({ + where: { + users: { + some: { + userId + } + } + } + }); + } + + async getOrgById(id: string) { + return this._organization.model.organization.findUnique({ + where: { + id + } + }); + } + async createOrgAndUser(body: Omit<CreateOrgUserDto, 'providerToken'> & {providerId?: string}) { return this._organization.model.organization.create({ data: { @@ -31,6 +51,7 @@ export class OrganizationRepository { } }, select: { + id: true, users: { select: { user: true diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index fbef5c67..04436021 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -1,13 +1,26 @@ import {CreateOrgUserDto} from "@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto"; import {Injectable} from "@nestjs/common"; import {OrganizationRepository} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository"; +import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service"; @Injectable() export class OrganizationService { constructor( - private _organizationRepository: OrganizationRepository + private _organizationRepository: OrganizationRepository, + private _notificationsService: NotificationService ){} async createOrgAndUser(body: Omit<CreateOrgUserDto, 'providerToken'> & {providerId?: string}) { - return this._organizationRepository.createOrgAndUser(body); + const register = await this._organizationRepository.createOrgAndUser(body); + await this._notificationsService.identifyUser(register.users[0].user); + await this._notificationsService.registerUserToTopic(register.users[0].user.id, `organization:${register.id}`); + return register; + } + + getOrgById(id: string) { + return this._organizationRepository.getOrgById(id); + } + + getFirstOrgByUserId(userId: string) { + return this._organizationRepository.getFirstOrgByUserId(userId); } } \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 24af4ac3..69396ff9 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -21,7 +21,7 @@ model Organization { updatedAt DateTime @updatedAt github GitHub[] subscription Subscription? - channel Channel? + Integration Integration[] tags Tag[] postTags PostTag[] postMedia PostMedia[] @@ -56,39 +56,46 @@ model UserOrganization { model GitHub { id String @id @default(uuid()) - login String - name String + login String? + name String? token String - jobId String + jobId String? organization Organization @relation(fields: [organizationId], references: [id]) organizationId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - stars Star[] @@index([login]) } model Trending { - id String @id @default(uuid()) - login String - feed Int - language Int - date DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - @@index([login]) + id String @id @default(uuid()) + trendingList String + language String? + hash String + date DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([language]) +} + +model TrendingLog { + id String @id @default(uuid()) + language String? + date DateTime } model Star { id String @id @default(uuid()) - githubId String - github GitHub @relation(fields: [githubId], references: [id]) stars Int totalStars Int - date DateTime @default(now()) + login String + date DateTime @default(now()) @db.Date createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@unique([login, date]) } model Media { @@ -115,16 +122,20 @@ model Subscription { updatedAt DateTime @updatedAt } -model Channel { +model Integration { id String @id @default(cuid()) - organizationId String @unique + internalId String + organizationId String + name String organization Organization @relation(fields: [organizationId], references: [id]) - channelProvider ChannelProvider - type Type + providerIdentifier String + type String token String + tokenExpiration DateTime? refreshToken String? - additionalData Json? posts Post[] + + @@unique([organizationId, internalId]) } model Tag { @@ -175,9 +186,9 @@ model Post { queueId String? publishDate DateTime organizationId String - channelId String + IntegrationId String organization Organization @relation(fields: [organizationId], references: [id]) - channel Channel @relation(fields: [channelId], references: [id]) + Integration Integration @relation(fields: [IntegrationId], references: [id]) title String? description String? canonicalUrl String? @@ -194,29 +205,12 @@ model Post { updatedAt DateTime @updatedAt } -enum Type { - ARTICLE - SOCIAL -} - enum State { QUEUE SENT DRAFT } -enum ChannelProvider { - TWITTER - LINKEDIN - DEV - HASHNODE - MEDIUM - HACKERNOON - YOUTUBE - GITHUB - DISCORD -} - enum SubscriptionTier { BASIC PRO diff --git a/libraries/nestjs-libraries/src/database/prisma/stars/stars.repository.ts b/libraries/nestjs-libraries/src/database/prisma/stars/stars.repository.ts index c07c2f35..75006656 100644 --- a/libraries/nestjs-libraries/src/database/prisma/stars/stars.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/stars/stars.repository.ts @@ -1,16 +1,196 @@ import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service"; import {Injectable} from "@nestjs/common"; +import {StarsListDto} from "@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto"; @Injectable() export class StarsRepository { constructor( - private _github: PrismaRepository<'gitHub'> + private _github: PrismaRepository<'gitHub'>, + private _stars: PrismaRepository<'star'>, + private _trending: PrismaRepository<'trending'>, + private _trendingLog: PrismaRepository<'trendingLog'>, ) { } + getGitHubRepositoriesByOrgId(org: string) { + return this._github.model.gitHub.findMany({ + where: { + organizationId: org + } + }); + } + replaceOrAddTrending(language: string, hashedNames: string, arr: { name: string; position: number }[]) { + return this._trending.model.trending.upsert({ + create: { + language, + hash: hashedNames, + trendingList: JSON.stringify(arr), + date: new Date() + }, + update: { + language, + hash: hashedNames, + trendingList: JSON.stringify(arr), + date: new Date() + }, + where: { + language + } + }); + } + + newTrending(language: string) { + return this._trendingLog.model.trendingLog.create({ + data: { + date: new Date(), + language + } + }); + } getAllGitHubRepositories() { return this._github.model.gitHub.findMany({ distinct: ['login'], }); } + + async getLastStarsByLogin(login: string) { + return (await this._stars.model.star.findMany({ + where: { + login, + }, + orderBy: { + date: 'desc', + }, + take: 1, + }))?.[0]; + } + + async getStarsByLogin(login: string) { + return (await this._stars.model.star.findMany({ + where: { + login, + }, + orderBy: { + date: 'asc', + } + })); + } + + async getGitHubsByNames(names: string[]) { + return this._github.model.gitHub.findMany({ + where: { + login: { + in: names + } + } + }); + } + + createStars(login: string, totalNewsStars: number, totalStars: number, date: Date) { + return this._stars.model.star.upsert({ + create: { + login, + stars: totalNewsStars, + totalStars, + date + }, + update: { + stars: totalNewsStars, + totalStars, + }, + where: { + login_date: { + date, + login + } + } + }); + } + + getTrendingByLanguage(language: string) { + return this._trending.model.trending.findUnique({ + where: { + language + } + }); + } + + getLastTrending(language: string) { + return this._trendingLog.model.trendingLog.findMany({ + where: { + language + }, + orderBy: { + date: 'desc' + }, + take: 100 + }); + } + + getStarsFilter(githubs: string[], starsFilter: StarsListDto) { + return this._stars.model.star.findMany({ + orderBy: { + [starsFilter.sortBy || 'date']: 'desc' + }, + where: { + login: { + in: githubs.filter(f => f) + } + }, + take: 20, + skip: starsFilter.page * 10 + }); + } + + addGitHub(orgId: string, accessToken: string) { + return this._github.model.gitHub.create({ + data: { + token: accessToken, + organizationId: orgId, + jobId: '' + } + }); + } + + getGitHubById(orgId: string, id: string) { + return this._github.model.gitHub.findUnique({ + where: { + organizationId: orgId, + id + } + }); + } + + updateGitHubLogin(orgId: string, id: string, login: string) { + return this._github.model.gitHub.update({ + where: { + organizationId: orgId, + id + }, + data: { + login + } + }); + } + + deleteRepository(orgId: string, id: string) { + return this._github.model.gitHub.delete({ + where: { + organizationId: orgId, + id + } + }); + } + + getOrganizationsByGitHubLogin(login: string) { + return this._github.model.gitHub.findMany({ + select: { + organizationId: true + }, + where: { + login + }, + distinct: ['organizationId'] + }); + } } \ No newline at end of file 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 c3b85ffd..404c7deb 100644 --- a/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/stars/stars.service.ts @@ -1,13 +1,230 @@ 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 * as console from "console"; +enum Inform { + Removed, + New, + Changed +} @Injectable() export class StarsService { constructor( - private _starsRepository: StarsRepository + private _starsRepository: StarsRepository, + private _notificationsService: NotificationService ){} + 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) { + console.log('processing', login, page); + 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)); + } + + 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); + } + } + + 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' + }, + 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) { + 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 diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts new file mode 100644 index 00000000..e2d913ec --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -0,0 +1,60 @@ +export interface PricingInterface { + [key: string]: { + pricing: { + monthly: number; + yearly: number; + }, + friends: boolean; + crossPosting: boolean; + repository: number; + ai: boolean; + integrations: number; + totalPosts: number; + medias: number; + influencers: boolean; + } +} +export const pricing: PricingInterface = { + FREE: { + pricing: { + monthly: 0, + yearly: 0, + }, + friends: false, + crossPosting: false, + repository: 1, + ai: false, + integrations: 2, + totalPosts: 20, + medias: 2, + influencers: false, + }, + BASIC: { + pricing: { + monthly: 50, + yearly: 500, + }, + friends: false, + crossPosting: true, + repository: 2, + ai: false, + integrations: 4, + totalPosts: 100, + medias: 5, + influencers: true, + }, + PRO: { + pricing: { + monthly: 100, + yearly: 1000, + }, + friends: true, + crossPosting: true, + repository: 4, + ai: true, + integrations: 10, + totalPosts: 300, + medias: 10, + influencers: true, + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts new file mode 100644 index 00000000..00bb28be --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts @@ -0,0 +1,96 @@ +import {Injectable} from "@nestjs/common"; +import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service"; + +@Injectable() +export class SubscriptionRepository { + constructor( + private readonly _subscription: PrismaRepository<'subscription'>, + private readonly _organization: PrismaRepository<'organization'>, + ) { + } + + getSubscriptionByOrganizationId(organizationId: string) { + return this._subscription.model.subscription.findFirst({ + where: { + organizationId, + subscriptionState: 'ACTIVE' + }, + }); + } + + checkSubscription(organizationId: string, subscriptionId: string) { + return this._subscription.model.subscription.findFirst({ + where: { + organizationId, + identifier: subscriptionId, + subscriptionState: 'ACTIVE' + }, + }); + } + + deleteSubscriptionByCustomerId(customerId: string) { + return this._subscription.model.subscription.deleteMany({ + where: { + organization: { + paymentId: customerId + } + } + }); + } + + updateCustomerId(organizationId: string, customerId: string) { + return this._organization.model.organization.update({ + where: { + id: organizationId + }, + data: { + paymentId: customerId + } + }); + } + + async getSubscriptionByCustomerId(customerId: string) { + return this._subscription.model.subscription.findFirst({ + where: { + organization: { + paymentId: customerId + } + } + }); + } + + async getOrganizationByCustomerId(customerId: string) { + return this._organization.model.organization.findFirst({ + where: { + paymentId: customerId + } + }); + } + + async createOrUpdateSubscription(identifier: string, customerId: string, billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', cancelAt: number | null) { + const findOrg = (await this.getOrganizationByCustomerId(customerId))!; + await this._subscription.model.subscription.upsert({ + where: { + organizationId: findOrg.id, + organization: { + paymentId: customerId, + } + }, + update: { + subscriptionTier: billing, + period, + subscriptionState: 'ACTIVE', + identifier, + cancelAt: cancelAt ? new Date(cancelAt * 1000) : null, + }, + create: { + organizationId: findOrg.id, + subscriptionTier: billing, + period, + subscriptionState: 'ACTIVE', + cancelAt: cancelAt ? new Date(cancelAt * 1000) : null, + identifier + } + }); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts new file mode 100644 index 00000000..24c31ddc --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts @@ -0,0 +1,55 @@ +import {Injectable} from "@nestjs/common"; +import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing"; +import {SubscriptionRepository} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository"; + +@Injectable() +export class SubscriptionService { + constructor( + private readonly _subscriptionRepository: SubscriptionRepository + ) {} + + getSubscriptionByOrganizationId(organizationId: string) { + return this._subscriptionRepository.getSubscriptionByOrganizationId(organizationId); + } + + async deleteSubscription(customerId: string) { + await this.modifySubscription(customerId, 'FREE'); + return this._subscriptionRepository.deleteSubscriptionByCustomerId(customerId); + } + + updateCustomerId(organizationId: string, customerId: string) { + return this._subscriptionRepository.updateCustomerId(organizationId, customerId); + } + + checkSubscription(organizationId: string, subscriptionId: string) { + return this._subscriptionRepository.checkSubscription(organizationId, subscriptionId); + } + + async modifySubscription(customerId: string, billing: 'FREE' | 'BASIC' | 'PRO') { + const getCurrentSubscription = (await this._subscriptionRepository.getSubscriptionByCustomerId(customerId))!; + const from = pricing[getCurrentSubscription?.subscriptionTier || 'FREE']; + const to = pricing[billing]; + + // if (to.faq < from.faq) { + // await this._faqRepository.deleteFAQs(getCurrentSubscription?.organizationId, from.faq - to.faq); + // } + // if (to.categories < from.categories) { + // await this._categoriesRepository.deleteCategories(getCurrentSubscription?.organizationId, from.categories - to.categories); + // } + // if (to.integrations < from.integrations) { + // await this._integrationsRepository.deleteIntegrations(getCurrentSubscription?.organizationId, from.integrations - to.integrations); + // } + // if (to.user < from.user) { + // await this._integrationsRepository.deleteUsers(getCurrentSubscription?.organizationId, from.user - to.user); + // } + // if (to.domains < from.domains) { + // await this._settingsService.deleteDomainByOrg(getCurrentSubscription?.organizationId); + // await this._organizationRepository.changePowered(getCurrentSubscription?.organizationId); + // } + } + + async createOrUpdateSubscription(identifier: string, customerId: string, billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', cancelAt: number | null) { + await this.modifySubscription(customerId, billing); + return this._subscriptionRepository.createOrUpdateSubscription(identifier, customerId, billing, period, cancelAt); + } +} diff --git a/libraries/nestjs-libraries/src/dtos/analytics/stars.list.dto.ts b/libraries/nestjs-libraries/src/dtos/analytics/stars.list.dto.ts new file mode 100644 index 00000000..0cd39e73 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/analytics/stars.list.dto.ts @@ -0,0 +1,11 @@ +import {IsDefined, IsIn, IsNumber, IsOptional} from "class-validator"; + +export class StarsListDto { + @IsNumber() + @IsDefined() + page: number; + + @IsOptional() + @IsIn(['totalStars', 'stars', 'date']) + sortBy: 'date' | 'stars' | 'totalStars'; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts b/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts new file mode 100644 index 00000000..86e582db --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts @@ -0,0 +1,9 @@ +import {IsIn} from "class-validator"; + +export class BillingSubscribeDto { + @IsIn(['MONTHLY', 'YEARLY']) + period: 'MONTHLY' | 'YEARLY'; + + @IsIn(['BASIC', 'PRO']) + billing: 'BASIC' | 'PRO'; +} diff --git a/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts b/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts new file mode 100644 index 00000000..db2fb937 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts @@ -0,0 +1,11 @@ +import {IsDefined, IsString} from "class-validator"; + +export class ConnectIntegrationDto { + @IsString() + @IsDefined() + state: string; + + @IsString() + @IsDefined() + code: string; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts new file mode 100644 index 00000000..652522f9 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/article/article.integrations.interface.ts @@ -0,0 +1,9 @@ +export interface ArticleIntegrationsInterface { + authenticate(token: string): Promise<{id: string, name: string, token: string}>; + publishPost(token: string, content: string): Promise<string>; +} + +export interface ArticleProvider extends ArticleIntegrationsInterface { + identifier: string; + name: string; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts new file mode 100644 index 00000000..76ac339e --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts @@ -0,0 +1,23 @@ +import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface"; + +export class DevToProvider implements ArticleProvider { + identifier = 'devto'; + name = 'Dev.to'; + async authenticate(token: string): Promise<{ id: string; name: string; token: string; }> { + const {name, id} = await (await fetch('https://dev.to/api/users/me', { + headers: { + 'api-key': token + } + })).json(); + + return { + id, + name, + token + } + } + + async publishPost(token: string, content: string): Promise<string> { + return ''; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts b/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts new file mode 100644 index 00000000..c0b96347 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts @@ -0,0 +1,42 @@ +import {ArticleIntegrationsInterface, ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface"; + +export class HashnodeProvider implements ArticleProvider { + identifier = 'hashnode'; + name = 'Hashnode'; + async authenticate(token: string): Promise<{ id: string; name: string; token: string; }> { + try { + const {data: {me: {name, id}}} = await (await fetch('https://gql.hashnode.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}` + }, + body: JSON.stringify({ + query: ` + query { + me { + name, + id + } + } + ` + }) + })).json(); + + return { + id, name, token + } + } + catch (err) { + return { + id: '', + name: '', + token: '' + } + } + } + + async publishPost(token: string, content: string): Promise<string> { + return ''; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts new file mode 100644 index 00000000..005534ee --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts @@ -0,0 +1,24 @@ +import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface"; + +export class MediumProvider implements ArticleProvider { + identifier = 'medium'; + name = 'Medium'; + + async authenticate(token: string): Promise<{ id: string; name: string; token: string; }> { + const {data: {name, id}} = await (await fetch('https://api.medium.com/v1/me', { + headers: { + Authorization: `Bearer ${token}` + } + })).json(); + + return { + id, + name, + token + } + } + + async publishPost(token: string, content: string): Promise<string> { + return ''; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts new file mode 100644 index 00000000..d19a33a1 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -0,0 +1,37 @@ +import {Injectable} from "@nestjs/common"; +import {XProvider} from "@gitroom/nestjs-libraries/integrations/social/x.provider"; +import {SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface"; +import {LinkedinProvider} from "@gitroom/nestjs-libraries/integrations/social/linkedin.provider"; +import {RedditProvider} from "@gitroom/nestjs-libraries/integrations/social/reddit.provider"; +import {DevToProvider} from "@gitroom/nestjs-libraries/integrations/article/dev.to.provider"; +import {HashnodeProvider} from "@gitroom/nestjs-libraries/integrations/article/hashnode.provider"; +import {MediumProvider} from "@gitroom/nestjs-libraries/integrations/article/medium.provider"; +import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface"; + +const socialIntegrationList = [ + new XProvider(), + new LinkedinProvider(), + new RedditProvider() +]; + +const articleIntegrationList = [ + new DevToProvider(), + new HashnodeProvider(), + new MediumProvider() +]; + +@Injectable() +export class IntegrationManager { + getAllowedSocialsIntegrations() { + return socialIntegrationList.map(p => p.identifier); + } + getSocialIntegration(integration: string): SocialProvider { + return socialIntegrationList.find(i => i.identifier === integration)!; + } + getAllowedArticlesIntegrations() { + return articleIntegrationList.map(p => p.identifier); + } + getArticlesIntegration(integration: string): ArticleProvider { + return articleIntegrationList.find(i => i.identifier === integration)!; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts new file mode 100644 index 00000000..7c85c686 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -0,0 +1,82 @@ +import {AuthTokenDetails, PostDetails, PostResponse, SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface"; +import {makeId} from "@gitroom/nestjs-libraries/services/make.is"; + +export class LinkedinProvider implements SocialProvider { + identifier = 'linkedin'; + name = 'LinkedIn'; + async refreshToken(refresh_token: string): Promise<AuthTokenDetails> { + const {access_token: accessToken, refresh_token: refreshToken} = await (await fetch('https://www.linkedin.com/oauth/v2/accessToken', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token, + client_id: process.env.LINKEDIN_CLIENT_ID!, + client_secret: process.env.LINKEDIN_CLIENT_SECRET! + }) + })).json() + + const {id, localizedFirstName, localizedLastName} = await (await fetch('https://api.linkedin.com/v2/me', { + headers: { + Authorization: `Bearer ${accessToken}` + } + })).json(); + + return { + id, + accessToken, + refreshToken, + name: `${localizedFirstName} ${localizedLastName}` + } + } + + async generateAuthUrl() { + const state = makeId(6); + const codeVerifier = makeId(30); + const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${process.env.LINKEDIN_CLIENT_ID}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/integrations/social/linkedin`)}&state=${state}&scope=${encodeURIComponent('openid profile w_member_social')}`; + return { + url, + codeVerifier, + state + } + } + + async authenticate(params: {code: string, codeVerifier: string}) { + const body = new URLSearchParams(); + body.append('grant_type', 'authorization_code'); + body.append('code', params.code); + body.append('redirect_uri', `${process.env.FRONTEND_URL}/integrations/social/linkedin`); + body.append('client_id', process.env.LINKEDIN_CLIENT_ID!); + body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!); + + const {access_token: accessToken, expires_in: expiresIn, refresh_token: refreshToken, ...data} = await (await fetch('https://www.linkedin.com/oauth/v2/accessToken', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body + })).json() + + console.log({accessToken, expiresIn, refreshToken, data}); + + const {name, sub: id} = await (await fetch('https://api.linkedin.com/v2/userinfo', { + headers: { + Authorization: `Bearer ${accessToken}` + } + })).json(); + + return { + id, + accessToken, + refreshToken, + expiresIn, + name + } + } + + async schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> { + return []; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts new file mode 100644 index 00000000..83fe495b --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -0,0 +1,81 @@ +import {AuthTokenDetails, PostDetails, PostResponse, SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface"; +import {makeId} from "@gitroom/nestjs-libraries/services/make.is"; + +export class RedditProvider implements SocialProvider { + identifier = 'reddit'; + name = 'Reddit'; + async refreshToken(refreshToken: string): Promise<AuthTokenDetails> { + const {access_token: accessToken, refresh_token: newRefreshToken, expires_in: expiresIn} = await (await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString('base64')}` + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken + }) + })).json(); + + const {name, id} = await (await fetch('https://oauth.reddit.com/api/v1/me', { + headers: { + Authorization: `Bearer ${accessToken}` + } + })).json(); + + return { + id, + name, + accessToken, + refreshToken: newRefreshToken, + expiresIn + } + } + + async generateAuthUrl() { + const state = makeId(6); + const codeVerifier = makeId(30); + const url = `https://www.reddit.com/api/v1/authorize?client_id=${process.env.REDDIT_CLIENT_ID}&response_type=code&state=${state}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/integrations/social/reddit`)}&duration=permanent&scope=${encodeURIComponent('identity submit flair')}`; + return { + url, + codeVerifier, + state + } + } + + async authenticate(params: {code: string, codeVerifier: string}) { + const {access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn} = await (await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString('base64')}` + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: params.code, + redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/reddit` + }) + })).json(); + + const {name, id} = await (await fetch('https://oauth.reddit.com/api/v1/me', { + headers: { + Authorization: `Bearer ${accessToken}` + } + })).json(); + + return { + id, + name, + accessToken, + refreshToken, + expiresIn + } + } + + async schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> { + return [{ + postId: '123', + status: 'scheduled' + }]; + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts new file mode 100644 index 00000000..13b62f9c --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -0,0 +1,51 @@ +export interface IAuthenticator { + authenticate(params: {code: string, codeVerifier: string}): Promise<AuthTokenDetails>; + refreshToken(refreshToken: string): Promise<AuthTokenDetails>; + generateAuthUrl(): Promise<GenerateAuthUrlResponse>; +} + +export type GenerateAuthUrlResponse = { + url: string, + codeVerifier: string, + state: string +} + +export type AuthTokenDetails = { + id: string; + name: string; + accessToken: string; // The obtained access token + refreshToken?: string; // The refresh token, if applicable + expiresIn?: number; // The duration in seconds for which the access token is valid +}; + +export interface ISocialMediaIntegration { + schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]>; // Schedules a new post +} + +export type PostResponse = { + postId: string; // The ID of the scheduled post returned by the platform + status: string; // Status of the operation or initial post status +}; + +export type PostDetails = { + message: string; + scheduledTime: Date; // The time when the post should be published + media?: MediaContent[]; // Optional array of media content to be attached with the post + poll?: PollDetails; // Optional poll details +}; + +export type PollDetails = { + options: string[]; // Array of poll options + duration: number; // Duration in hours for which the poll will be active +} + +export type MediaContent = { + type: 'image' | 'video'; // Type of the media content + url: string; // URL of the media file, if it's already hosted somewhere + file?: File; // The actual media file to upload, if not hosted +}; + +export interface SocialProvider extends IAuthenticator, ISocialMediaIntegration { + identifier: string; + name: string; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts new file mode 100644 index 00000000..a7031a18 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -0,0 +1,67 @@ +import { TwitterApi } from 'twitter-api-v2'; +import {AuthTokenDetails, PostDetails, PostResponse, SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface"; + +export class XProvider implements SocialProvider { + identifier = 'x'; + name = 'X'; + async refreshToken(refreshToken: string): Promise<AuthTokenDetails> { + const startingClient = new TwitterApi({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET! }); + const { accessToken, refreshToken: newRefreshToken, expiresIn, client } = await startingClient.refreshOAuth2Token(refreshToken); + const {data: {id, name}} = await client.v2.me(); + return { + id, + name, + accessToken, + refreshToken: newRefreshToken, + expiresIn + } + } + + async generateAuthUrl() { + const client = new TwitterApi({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET! }); + const {url, codeVerifier, state} = client.generateOAuth2AuthLink( + process.env.FRONTEND_URL + '/integrations/social/x', + { scope: ['tweet.read', 'users.read', 'tweet.write', 'offline.access'] }); + return { + url, + codeVerifier, + state + } + } + + async authenticate(params: {code: string, codeVerifier: string}) { + const startingClient = new TwitterApi({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET! }); + const {accessToken, refreshToken, expiresIn, client} = await startingClient.loginWithOAuth2({ + code: params.code, + codeVerifier: params.codeVerifier, + redirectUri: process.env.FRONTEND_URL + '/integrations/social/x' + }); + + const {data: {id, name}} = await client.v2.me(); + + return { + id, + accessToken, + name, + refreshToken, + expiresIn + } + } + + async schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> { + const client = new TwitterApi(accessToken); + const ids: string[] = []; + for (const post of postDetails) { + const {data}: {data: {id: string}} = await client.v2.tweet({ + text: post.message, + ...ids.length ? { reply: {in_reply_to_tweet_id: ids[ids.length - 1]} } : {}, + }); + ids.push(data.id); + } + + return ids.map(p => ({ + postId: p, + status: 'posted' + })); + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/notifications/notification.service.ts b/libraries/nestjs-libraries/src/notifications/notification.service.ts new file mode 100644 index 00000000..f86b8f90 --- /dev/null +++ b/libraries/nestjs-libraries/src/notifications/notification.service.ts @@ -0,0 +1,34 @@ +import {Injectable} from "@nestjs/common"; +import {Novu, TriggerRecipientsTypeEnum} from '@novu/node'; +import {User} from "@prisma/client"; + +const novu = new Novu(process.env.NOVU_API_KEY!); + +@Injectable() +export class NotificationService { + async registerUserToTopic(userId: string, topic: string) { + try { + await novu.topics.create({ + name: 'organization topic', + key: topic + }); + } + catch (err) { /* empty */ } + await novu.topics.addSubscribers(topic, { + subscribers: [userId] + }); + } + + async identifyUser(user: User) { + await novu.subscribers.identify(user.id, { + email: user.email, + }); + } + + async sendNotificationToTopic(workflow: string, topic: string, payload = {}) { + await novu.trigger(workflow, { + to: [{type: TriggerRecipientsTypeEnum.TOPIC, topicKey: topic}], + payload + }); + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/services/make.is.ts b/libraries/nestjs-libraries/src/services/make.is.ts new file mode 100644 index 00000000..1044f7f9 --- /dev/null +++ b/libraries/nestjs-libraries/src/services/make.is.ts @@ -0,0 +1,9 @@ +export const makeId = (length: number) => { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i += 1) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +}; diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index 43996461..b998321f 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -1,8 +1,144 @@ import Stripe from 'stripe'; import {Injectable} from "@nestjs/common"; -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); +import {Organization} from "@prisma/client"; +import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service"; +import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; +import {makeId} from "@gitroom/nestjs-libraries/services/make.is"; +import {BillingSubscribeDto} from "@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto"; + +const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, { + apiVersion: '2023-10-16' +}); @Injectable() export class StripeService { + constructor( + private _subscriptionService: SubscriptionService, + private _organizationService: OrganizationService, + ) { + } + validateRequest(rawBody: Buffer, signature: string, endpointSecret: string) { + return stripe.webhooks.constructEvent(rawBody, signature, endpointSecret); + } + createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const {id, billing, period}: { billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', id: string} = event.data.object.metadata; + return this._subscriptionService.createOrUpdateSubscription(id, event.data.object.customer as string, billing, period, event.data.object.cancel_at); + } + updateSubscription(event: Stripe.CustomerSubscriptionUpdatedEvent) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const {id, billing, period}: { billing: 'BASIC' | 'PRO', period: 'MONTHLY' | 'YEARLY', id: string} = event.data.object.metadata; + return this._subscriptionService.createOrUpdateSubscription(id, event.data.object.customer as string, billing, period, event.data.object.cancel_at); + } -} \ No newline at end of file + async deleteSubscription(event: Stripe.CustomerSubscriptionDeletedEvent) { + await this._subscriptionService.deleteSubscription(event.data.object.customer as string); + } + + async createOrGetCustomer(organization: Organization) { + if (organization.paymentId) { + return organization.paymentId; + } + + const customer = await stripe.customers.create(); + await this._subscriptionService.updateCustomerId(organization.id, customer.id); + return customer.id; + } + + async setToCancel(organizationId: string) { + const id = makeId(10); + const org = await this._organizationService.getOrgById(organizationId); + const customer = await this.createOrGetCustomer(org!); + const currentUserSubscription = await stripe.subscriptions.list({ + customer, + status: 'active', + }); + + await stripe.subscriptions.update(currentUserSubscription.data[0].id, { + cancel_at_period_end: !currentUserSubscription.data[0].cancel_at_period_end, + metadata: { + service: 'gitroom', + id + } + }); + + return {id}; + } + + async getCustomerByOrganizationId(organizationId: string) { + const org = (await this._organizationService.getOrgById(organizationId))!; + return org.paymentId; + } + + async createBillingPortalLink(customer: string) { + return stripe.billingPortal.sessions.create({ + customer, + }); + } + + async subscribe(organizationId: string, body: BillingSubscribeDto) { + const id = makeId(10); + + const org = await this._organizationService.getOrgById(organizationId); + const customer = await this.createOrGetCustomer(org!); + const allProducts = await stripe.products.list({ + active: true, + expand: ['data.prices'], + }); + const findProduct = allProducts.data.find(product => product.name.toLowerCase() === body.billing.toLowerCase()); + const pricesList = await stripe.prices.list({ + active: true, + product: findProduct!.id, + }); + + const findPrice = pricesList.data.find(p => p?.recurring?.interval?.toLowerCase() === body?.period?.toLowerCase().replace('ly', '')); + const currentUserSubscription = await stripe.subscriptions.list({ + customer, + status: 'active', + }); + + if (!currentUserSubscription.data.length) { + const {url} = await stripe.checkout.sessions.create({ + customer, + success_url: process.env['FRONTEND_URL'] + `/billing?check=${id}`, + mode: 'subscription', + subscription_data: { + metadata: { + service: 'gitroom', + ...body, + id + } + }, + line_items: [ + { + price: findPrice!.id, + quantity: 1, + }], + }); + + return {url}; + } + + try { + await stripe.subscriptions.update(currentUserSubscription.data[0].id, { + metadata: { + service: 'gitroom', + ...body, + id + }, items: [{ + id: currentUserSubscription.data[0].items.data[0].id, price: findPrice!.id, + }] + }); + + return {id}; + } + catch (err) { + const {url} = await this.createBillingPortalLink(customer); + return { + portal: url + } + } + } +} diff --git a/libraries/nestjs-libraries/src/services/trending.service.ts b/libraries/nestjs-libraries/src/services/trending.service.ts new file mode 100644 index 00000000..4484856c --- /dev/null +++ b/libraries/nestjs-libraries/src/services/trending.service.ts @@ -0,0 +1,29 @@ +import json from './trending'; +import {Injectable} from "@nestjs/common"; +import { JSDOM } from "jsdom"; +import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; +import md5 from "md5"; + +@Injectable() +export class TrendingService { + constructor( + private _starsService: StarsService, + ) { + } + async syncTrending() { + for (const language of json) { + const data = await (await fetch(`https://github.com/trending/${language.link}`)).text(); + const dom = new JSDOM(data); + const trending = Array.from(dom.window.document.querySelectorAll('[class="Link"]')); + const arr = trending.map((el, index) => { + return { + name: el?.textContent?.trim().replace(/\s/g, '') || '', + position: index + 1, + } + }); + + const hashedNames = md5(arr.map(p => p.name).join('')); + await this._starsService.updateTrending(language.name, hashedNames, arr); + } + } +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/services/trending.ts b/libraries/nestjs-libraries/src/services/trending.ts new file mode 100644 index 00000000..1bd84ba0 --- /dev/null +++ b/libraries/nestjs-libraries/src/services/trending.ts @@ -0,0 +1 @@ +export default [{"link":"","name":""},{"link":"1c-enterprise","name":"1C Enterprise"},{"link":"abap","name":"ABAP"},{"link":"actionscript","name":"ActionScript"},{"link":"adblock-filter-list","name":"Adblock Filter List"},{"link":"al","name":"AL"},{"link":"angelscript","name":"AngelScript"},{"link":"apacheconf","name":"ApacheConf"},{"link":"apex","name":"Apex"},{"link":"apl","name":"APL"},{"link":"applescript","name":"AppleScript"},{"link":"arc","name":"Arc"},{"link":"asl","name":"ASL"},{"link":"classic-asp","name":"Classic ASP"},{"link":"assembly","name":"Assembly"},{"link":"astro","name":"Astro"},{"link":"autohotkey","name":"AutoHotkey"},{"link":"autoit","name":"AutoIt"},{"link":"awk","name":"Awk"},{"link":"batchfile","name":"Batchfile"},{"link":"bicep","name":"Bicep"},{"link":"bikeshed","name":"Bikeshed"},{"link":"bitbake","name":"BitBake"},{"link":"blade","name":"Blade"},{"link":"boo","name":"Boo"},{"link":"brainfuck","name":"Brainfuck"},{"link":"brighterscript","name":"BrighterScript"},{"link":"c","name":"C"},{"link":"c%23","name":"C#"},{"link":"c++","name":"C++"},{"link":"cairo","name":"Cairo"},{"link":"cap'n-proto","name":"Cap'n Proto"},{"link":"cartocss","name":"CartoCSS"},{"link":"chapel","name":"Chapel"},{"link":"circom","name":"Circom"},{"link":"classic-asp","name":"Classic ASP"},{"link":"clojure","name":"Clojure"},{"link":"cmake","name":"CMake"},{"link":"codeql","name":"CodeQL"},{"link":"coffeescript","name":"CoffeeScript"},{"link":"common-lisp","name":"Common Lisp"},{"link":"component-pascal","name":"Component Pascal"},{"link":"crystal","name":"Crystal"},{"link":"css","name":"CSS"},{"link":"cuda","name":"Cuda"},{"link":"cue","name":"CUE"},{"link":"cython","name":"Cython"},{"link":"d","name":"D"},{"link":"dart","name":"Dart"},{"link":"denizenscript","name":"DenizenScript"},{"link":"digital-command-language","name":"DIGITAL Command Language"},{"link":"dm","name":"DM"},{"link":"dockerfile","name":"Dockerfile"},{"link":"earthly","name":"Earthly"},{"link":"ejs","name":"EJS"},{"link":"elixir","name":"Elixir"},{"link":"elm","name":"Elm"},{"link":"emacs-lisp","name":"Emacs Lisp"},{"link":"emberscript","name":"EmberScript"},{"link":"erlang","name":"Erlang"},{"link":"f%23","name":"F#"},{"link":"f*","name":"F*"},{"link":"fennel","name":"Fennel"},{"link":"fluent","name":"Fluent"},{"link":"forth","name":"Forth"},{"link":"fortran","name":"Fortran"},{"link":"freemarker","name":"FreeMarker"},{"link":"g-code","name":"G-code"},{"link":"gdscript","name":"GDScript"},{"link":"gherkin","name":"Gherkin"},{"link":"gleam","name":"Gleam"},{"link":"glsl","name":"GLSL"},{"link":"go","name":"Go"},{"link":"groovy","name":"Groovy"},{"link":"hack","name":"Hack"},{"link":"handlebars","name":"Handlebars"},{"link":"haskell","name":"Haskell"},{"link":"haxe","name":"Haxe"},{"link":"hcl","name":"HCL"},{"link":"hlsl","name":"HLSL"},{"link":"holyc","name":"HolyC"},{"link":"hoon","name":"hoon"},{"link":"hosts-file","name":"Hosts File"},{"link":"html","name":"HTML"},{"link":"idris","name":"Idris"},{"link":"inform-7","name":"Inform 7"},{"link":"inno-setup","name":"Inno Setup"},{"link":"io","name":"Io"},{"link":"java","name":"Java"},{"link":"javascript","name":"JavaScript"},{"link":"json","name":"JSON"},{"link":"jsonnet","name":"Jsonnet"},{"link":"julia","name":"Julia"},{"link":"jupyter-notebook","name":"Jupyter Notebook"},{"link":"just","name":"Just"},{"link":"kicad-layout","name":"KiCad Layout"},{"link":"kotlin","name":"Kotlin"},{"link":"labview","name":"LabVIEW"},{"link":"lean","name":"Lean"},{"link":"less","name":"Less"},{"link":"lfe","name":"LFE"},{"link":"liquid","name":"Liquid"},{"link":"llvm","name":"LLVM"},{"link":"logos","name":"Logos"},{"link":"lookml","name":"LookML"},{"link":"lua","name":"Lua"},{"link":"m4","name":"M4"},{"link":"makefile","name":"Makefile"},{"link":"markdown","name":"Markdown"},{"link":"mathematica","name":"Mathematica"},{"link":"matlab","name":"MATLAB"},{"link":"mcfunction","name":"mcfunction"},{"link":"mdx","name":"MDX"},{"link":"mermaid","name":"Mermaid"},{"link":"meson","name":"Meson"},{"link":"metal","name":"Metal"},{"link":"mlir","name":"MLIR"},{"link":"move","name":"Move"},{"link":"mustache","name":"Mustache"},{"link":"nasl","name":"NASL"},{"link":"nesc","name":"nesC"},{"link":"nextflow","name":"Nextflow"},{"link":"nim","name":"Nim"},{"link":"nix","name":"Nix"},{"link":"nsis","name":"NSIS"},{"link":"nunjucks","name":"Nunjucks"},{"link":"objective-c","name":"Objective-C"},{"link":"objective-c++","name":"Objective-C++"},{"link":"ocaml","name":"OCaml"},{"link":"odin","name":"Odin"},{"link":"open-policy-agent","name":"Open Policy Agent"},{"link":"openscad","name":"OpenSCAD"},{"link":"papyrus","name":"Papyrus"},{"link":"pascal","name":"Pascal"},{"link":"perl","name":"Perl"},{"link":"php","name":"PHP"},{"link":"plpgsql","name":"PLpgSQL"},{"link":"plsql","name":"PLSQL"},{"link":"pony","name":"Pony"},{"link":"postscript","name":"PostScript"},{"link":"powershell","name":"PowerShell"},{"link":"processing","name":"Processing"},{"link":"prolog","name":"Prolog"},{"link":"pug","name":"Pug"},{"link":"puppet","name":"Puppet"},{"link":"purebasic","name":"PureBasic"},{"link":"purescript","name":"PureScript"},{"link":"python","name":"Python"},{"link":"qml","name":"QML"},{"link":"r","name":"R"},{"link":"racket","name":"Racket"},{"link":"raku","name":"Raku"},{"link":"raml","name":"RAML"},{"link":"ren'py","name":"Ren'Py"},{"link":"rescript","name":"ReScript"},{"link":"restructuredtext","name":"reStructuredText"},{"link":"rich-text-format","name":"Rich Text Format"},{"link":"robotframework","name":"RobotFramework"},{"link":"roff","name":"Roff"},{"link":"routeros-script","name":"RouterOS Script"},{"link":"rpm-spec","name":"RPM Spec"},{"link":"ruby","name":"Ruby"},{"link":"rust","name":"Rust"},{"link":"sass","name":"Sass"},{"link":"scala","name":"Scala"},{"link":"scheme","name":"Scheme"},{"link":"scss","name":"SCSS"},{"link":"shaderlab","name":"ShaderLab"},{"link":"shell","name":"Shell"},{"link":"smali","name":"Smali"},{"link":"smalltalk","name":"Smalltalk"},{"link":"smarty","name":"Smarty"},{"link":"solidity","name":"Solidity"},{"link":"sqf","name":"SQF"},{"link":"sql","name":"SQL"},{"link":"squirrel","name":"Squirrel"},{"link":"standard-ml","name":"Standard ML"},{"link":"starlark","name":"Starlark"},{"link":"stylus","name":"Stylus"},{"link":"supercollider","name":"SuperCollider"},{"link":"svelte","name":"Svelte"},{"link":"svg","name":"SVG"},{"link":"swift","name":"Swift"},{"link":"swig","name":"SWIG"},{"link":"systemverilog","name":"SystemVerilog"},{"link":"tcl","name":"Tcl"},{"link":"tex","name":"TeX"},{"link":"text","name":"Text"},{"link":"thrift","name":"Thrift"},{"link":"tsql","name":"TSQL"},{"link":"twig","name":"Twig"},{"link":"typescript","name":"TypeScript"},{"link":"typst","name":"Typst"},{"link":"unrealscript","name":"UnrealScript"},{"link":"v","name":"V"},{"link":"vala","name":"Vala"},{"link":"vbscript","name":"VBScript"},{"link":"verilog","name":"Verilog"},{"link":"vhdl","name":"VHDL"},{"link":"vim-script","name":"Vim Script"},{"link":"vim-snippet","name":"Vim Snippet"},{"link":"visual-basic-.net","name":"Visual Basic .NET"},{"link":"visual-basic-.net","name":"Visual Basic .NET"},{"link":"vue","name":"Vue"},{"link":"webassembly","name":"WebAssembly"},{"link":"wgsl","name":"WGSL"},{"link":"witcher-script","name":"Witcher Script"},{"link":"xc","name":"XC"},{"link":"xslt","name":"XSLT"},{"link":"yacc","name":"Yacc"},{"link":"yaml","name":"YAML"},{"link":"yara","name":"YARA"},{"link":"zap","name":"ZAP"},{"link":"zenscript","name":"ZenScript"},{"link":"zig","name":"Zig"}]; diff --git a/libraries/nestjs-libraries/src/user/org.from.request.ts b/libraries/nestjs-libraries/src/user/org.from.request.ts new file mode 100644 index 00000000..ea3a87c5 --- /dev/null +++ b/libraries/nestjs-libraries/src/user/org.from.request.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const GetOrgFromRequest = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.org; + } +); diff --git a/libraries/react-shared-libraries/src/form/button.tsx b/libraries/react-shared-libraries/src/form/button.tsx new file mode 100644 index 00000000..03cb734a --- /dev/null +++ b/libraries/react-shared-libraries/src/form/button.tsx @@ -0,0 +1,8 @@ +import {ButtonHTMLAttributes, DetailedHTMLProps, FC} from "react"; +import {clsx} from "clsx"; + +export const Button: FC<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>> = (props) => { + return ( + <button {...props} type={props.type || 'button'} className={clsx('bg-forth px-[24px] h-[40px] cursor-pointer items-center justify-center flex', props?.className)} /> + ) +} \ No newline at end of file diff --git a/libraries/react-shared-libraries/src/form/checkbox.tsx b/libraries/react-shared-libraries/src/form/checkbox.tsx new file mode 100644 index 00000000..fd75efdc --- /dev/null +++ b/libraries/react-shared-libraries/src/form/checkbox.tsx @@ -0,0 +1,19 @@ +"use client"; +import {FC, useCallback, useState} from "react"; +import clsx from "clsx"; +import Image from "next/image"; + +export const Checkbox: FC<{checked: boolean, className?: string, onChange?: (event: {target: {value: string}}) => void}> = (props) => { + const {checked, className} = props; + const [currentStatus, setCurrentStatus] = useState(checked); + const changeStatus = useCallback(() => { + setCurrentStatus(!currentStatus); + props?.onChange?.({target: {value: `${!currentStatus}`}}); + }, [currentStatus]); + + return ( + <div onClick={changeStatus} className={clsx("cursor-pointer rounded-[4px] select-none bg-forth w-[24px] h-[24px] flex justify-center items-center", className)}> + {currentStatus && <Image src="/form/checked.svg" alt="Checked" width={20} height={20} />} + </div> + ) +} \ No newline at end of file diff --git a/libraries/react-shared-libraries/src/helpers/utc.date.render.tsx b/libraries/react-shared-libraries/src/helpers/utc.date.render.tsx new file mode 100644 index 00000000..1f50ee82 --- /dev/null +++ b/libraries/react-shared-libraries/src/helpers/utc.date.render.tsx @@ -0,0 +1,12 @@ +"use client"; + +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import {FC} from "react"; + +dayjs.extend(utc); + +export const UtcToLocalDateRender: FC<{date: string, format: string}> = (props) => { + const {date, format} = props; + return <>{dayjs.utc(date).local().format(format)}</>; +} \ No newline at end of file diff --git a/libraries/react-shared-libraries/tsconfig.json b/libraries/react-shared-libraries/tsconfig.json index d6072f46..8b20abce 100644 --- a/libraries/react-shared-libraries/tsconfig.json +++ b/libraries/react-shared-libraries/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "module": "commonjs", "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "strict": true, "noImplicitOverride": true, "noImplicitReturns": true, diff --git a/package-lock.json b/package-lock.json index f198507c..8df866c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,27 +12,37 @@ "apps/*" ], "dependencies": { + "@casl/ability": "^6.5.0", "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/microservices": "^10.3.1", "@nestjs/platform-express": "^10.0.2", "@nestjs/schedule": "^4.0.0", + "@novu/node": "^0.23.0", + "@novu/notification-center": "^0.23.0", "@prisma/client": "^5.8.1", "@swc/helpers": "~0.5.2", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.5", + "@types/lodash": "^4.14.202", + "@types/md5": "^2.3.5", "@types/stripe": "^8.0.417", "axios": "^1.0.0", "bcrypt": "^5.1.1", "bullmq": "^5.1.5", + "chart.js": "^4.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "clsx": "^2.1.0", "cookie-parser": "^1.4.6", + "dayjs": "^1.11.10", "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", + "md5": "^2.3.0", "nestjs-command": "^3.1.4", "next": "13.4.4", + "prisma-paginate": "^5.2.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.49.3", @@ -41,8 +51,10 @@ "redis": "^4.6.12", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", + "simple-statistics": "^7.8.3", "stripe": "^14.14.0", "tslib": "^2.3.0", + "twitter-api-v2": "^1.16.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -259,7 +271,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" @@ -272,7 +283,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -284,7 +294,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -298,7 +307,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -306,14 +314,12 @@ "node_modules/@babel/code-frame/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/code-frame/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -322,7 +328,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -566,7 +571,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, "dependencies": { "@babel/types": "^7.22.15" }, @@ -688,7 +692,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -697,7 +700,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -743,7 +745,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -757,7 +758,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -769,7 +769,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -783,7 +782,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -791,14 +789,12 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -807,7 +803,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -2337,7 +2332,6 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", - "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -2353,6 +2347,17 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2375,6 +2380,204 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/css": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", + "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", + "dependencies": { + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", + "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -2826,6 +3029,54 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", + "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.19.2.tgz", + "integrity": "sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==", + "dependencies": { + "@floating-ui/react-dom": "^1.3.0", + "aria-hidden": "^1.1.3", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz", + "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==", + "dependencies": { + "@floating-ui/dom": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3413,6 +3664,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -3427,6 +3683,66 @@ "node": ">=8" } }, + "node_modules/@mantine/core": { + "version": "5.10.5", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-5.10.5.tgz", + "integrity": "sha512-F4tqHSEVM9D6/iSqHfPda+Xl5XgSEPHAAkT01Zwzj4Jnbd10qGrlqr/SFUop2CIcuKYnmra9XltUahUPXBC2BQ==", + "dependencies": { + "@floating-ui/react": "^0.19.1", + "@mantine/styles": "5.10.5", + "@mantine/utils": "5.10.5", + "@radix-ui/react-scroll-area": "1.0.2", + "react-textarea-autosize": "8.3.4" + }, + "peerDependencies": { + "@mantine/hooks": "5.10.5", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@mantine/hooks": { + "version": "5.10.5", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-5.10.5.tgz", + "integrity": "sha512-hFQp71QZDfivPzfIUOQZfMKLiOL/Cn2EnzacRlbUr55myteTfzYN8YMt+nzniE/6c4IRopFHEAdbKEtfyQc6kg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@mantine/styles": { + "version": "5.10.5", + "resolved": "https://registry.npmjs.org/@mantine/styles/-/styles-5.10.5.tgz", + "integrity": "sha512-0NXk8c/XGzuTUkZc6KceF2NaTCMEu5mHR4ru0x+ttb9DGnLpHuGWduTHjSfr4hl6eAJgedD0zauO+VAhDzO9zA==", + "dependencies": { + "clsx": "1.1.1", + "csstype": "3.0.9" + }, + "peerDependencies": { + "@emotion/react": ">=11.9.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@mantine/styles/node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mantine/styles/node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + }, + "node_modules/@mantine/utils": { + "version": "5.10.5", + "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-5.10.5.tgz", + "integrity": "sha512-FGMq4dGs5HhDAtI0z46uzxzKKPmZ3h5uKUyKg1ZHoFR1mBtcUMbB6FylFmHqKFRWlJ5IXqX9dwmiVrLYUOfTmA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -3974,6 +4290,93 @@ "node": ">= 8" } }, + "node_modules/@novu/client": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@novu/client/-/client-0.23.0.tgz", + "integrity": "sha512-iy4v1+uB38vxDn6IRXqU1yVA2Z2ZtIyKQW9S8bZVhw+zVfrqdhzyKUE/ZkjR62UrNWTzPPhSvZGsqdVAD55PwA==", + "dependencies": { + "@novu/shared": "^0.23.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@novu/node": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@novu/node/-/node-0.23.0.tgz", + "integrity": "sha512-buH4wCrxsb5RL6TZ3w2YfMhYPvmmoYO5+CkBfvn6hibGXufft5+4DPx8t002aRANQO8SKdMH3+HPvgVhsORMWw==", + "dependencies": { + "@novu/shared": "^0.23.0", + "axios-retry": "^3.8.0", + "handlebars": "^4.7.7", + "lodash.get": "^4.4.2", + "lodash.merge": "^4.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@novu/node/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@novu/notification-center": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@novu/notification-center/-/notification-center-0.23.0.tgz", + "integrity": "sha512-uZ/3jDrwJ3DWGzP0ckd650pCF93GlvluBoXulL99oeVNAJNOmH/UyVygG3KVPkVA0so/pZ3b8TWi9v8z20cYdQ==", + "dependencies": { + "@emotion/css": "^11.10.5", + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@mantine/core": "^5.7.1", + "@mantine/hooks": "^5.7.1", + "@novu/client": "^0.23.0", + "@novu/shared": "^0.23.0", + "@tanstack/react-query": "^4.20.4", + "acorn-jsx": "^5.3.2", + "axios": "^1.6.2", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.merge": "^4.6.2", + "react-infinite-scroll-component": "^6.0.0", + "socket.io-client": "4.7.2", + "tslib": "^2.3.1", + "webfontloader": "^1.6.28" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@novu/shared": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@novu/shared/-/shared-0.23.0.tgz", + "integrity": "sha512-mw71K74RojlbuKx+mYwxj8aYS1rqi+lHN1TeQ4hsKkM8+Kf3Ypu4IZHGa1DJhUSi7+UOXHlj3ypjkWHB65PGKg==", + "dependencies": { + "axios": "^1.6.2", + "class-transformer": "0.5.1", + "class-validator": "0.14.0" + } + }, + "node_modules/@novu/shared/node_modules/class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "node_modules/@nrwl/devkit": { "version": "17.2.8", "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.2.8.tgz", @@ -4826,14 +5229,12 @@ "node_modules/@prisma/debug": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.8.1.tgz", - "integrity": "sha512-tjuw7eA0Us3T42jx9AmAgL58rzwzpFGYc3R7Y4Ip75EBYrKMBA1YihuWMcBC92ILmjlQ/u3p8VxcIE0hr+fZfg==", - "devOptional": true + "integrity": "sha512-tjuw7eA0Us3T42jx9AmAgL58rzwzpFGYc3R7Y4Ip75EBYrKMBA1YihuWMcBC92ILmjlQ/u3p8VxcIE0hr+fZfg==" }, "node_modules/@prisma/engines": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.8.1.tgz", "integrity": "sha512-TJgYLRrZr56uhqcXO4GmP5be+zjCIHtLDK20Cnfg+o9d905hsN065QOL+3Z0zQAy6YD31Ol4u2kzSfRmbJv/uA==", - "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "5.8.1", @@ -4845,14 +5246,12 @@ "node_modules/@prisma/engines-version": { "version": "5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2.tgz", - "integrity": "sha512-f5C3JM3l9yhGr3cr4FMqWloFaSCpNpMi58Om22rjD2DOz3owci2mFdFXMgnAGazFPKrCbbEhcxdsRfspEYRoFQ==", - "devOptional": true + "integrity": "sha512-f5C3JM3l9yhGr3cr4FMqWloFaSCpNpMi58Om22rjD2DOz3owci2mFdFXMgnAGazFPKrCbbEhcxdsRfspEYRoFQ==" }, "node_modules/@prisma/fetch-engine": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.8.1.tgz", "integrity": "sha512-+bgjjoSFa6uYEbAPlklfoVSStOEfcpheOjoBoNsNNSQdSzcwE2nM4Q0prun0+P8/0sCHo18JZ9xqa8gObvgOUw==", - "devOptional": true, "dependencies": { "@prisma/debug": "5.8.1", "@prisma/engines-version": "5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2", @@ -4863,11 +5262,141 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.8.1.tgz", "integrity": "sha512-wnA+6HTFcY+tkykMokix9GiAkaauPC5W/gg0O5JB0J8tCTNWrqpnQ7AsaGRfkYUbeOIioh6woDjQrGTTRf1Zag==", - "devOptional": true, "dependencies": { "@prisma/debug": "5.8.1" } }, + "node_modules/@radix-ui/number": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", + "integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", + "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz", + "integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -5145,6 +5674,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -5807,6 +6341,41 @@ "node": ">=10" } }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "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", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", @@ -6207,11 +6776,21 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + }, "node_modules/@types/luxon": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.8.tgz", "integrity": "sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==" }, + "node_modules/@types/md5": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", + "integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -6235,8 +6814,7 @@ "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { "version": "15.7.11", @@ -6711,6 +7289,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.4.tgz", + "integrity": "sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", @@ -7163,7 +7772,6 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -7194,7 +7802,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -7435,6 +8042,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -7660,6 +8278,15 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-retry": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz", + "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "is-retry-allowed": "^2.2.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -8520,7 +9147,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -8619,6 +9245,25 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -9369,6 +10014,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/css-declaration-sorter": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", @@ -9719,8 +10372,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -9742,6 +10394,11 @@ "node": ">=14" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -10312,6 +10969,46 @@ "once": "^1.4.0" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -10366,7 +11063,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -10554,7 +11250,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -11823,6 +12518,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -12413,6 +13113,26 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -12527,6 +13247,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hosted-git-info": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", @@ -12876,7 +13609,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -13072,8 +13804,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-bigint": { "version": "1.0.4", @@ -13115,6 +13846,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -13131,7 +13867,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -13313,6 +14048,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", @@ -14518,8 +15264,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -14924,6 +15669,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.compact": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.compact/-/lodash.compact-3.0.1.tgz", @@ -14932,8 +15682,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.defaults": { "version": "4.2.0", @@ -14945,6 +15694,11 @@ "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -14989,8 +15743,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/lodash.once": { "version": "4.1.1", @@ -15134,6 +15887,16 @@ "remove-accents": "0.5.0" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -15553,8 +16316,7 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/nestjs-command": { "version": "3.1.4", @@ -16254,7 +17016,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -16266,7 +17027,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -16283,8 +17043,7 @@ "node_modules/parse-json/node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/parse-node-version": { "version": "1.0.1", @@ -16344,8 +17103,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", @@ -16381,7 +17139,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -17302,7 +18059,6 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.8.1.tgz", "integrity": "sha512-N6CpjzECnUHZ5beeYpDzkt2rYpEdAeqXX2dweu6BoQaeYkNZrC/WJHM+5MO/uidFHTak8QhkPKBWck1o/4MD4A==", - "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.8.1" @@ -17314,6 +18070,15 @@ "node": ">=16.13" } }, + "node_modules/prisma-paginate": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/prisma-paginate/-/prisma-paginate-5.2.1.tgz", + "integrity": "sha512-GwR39iSOGUqTkfpEl75hA/LyMUkhAwc2DqyhDkO6ug+u3zBUZlIfEWolqHOCrHOAKQlaNpgnP2nxFLH0LjBMtA==", + "peerDependencies": { + "@prisma/client": ">=4.9.0", + "prisma": ">=4.9.0" + } + }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -17562,6 +18327,17 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -17632,6 +18408,22 @@ "react-dom": ">=16.8" } }, + "node_modules/react-textarea-autosize": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz", + "integrity": "sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==", + "dependencies": { + "@babel/runtime": "^7.10.2", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -17847,7 +18639,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -17891,7 +18682,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -18529,6 +19319,14 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-statistics": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.3.tgz", + "integrity": "sha512-JFvMY00t6SBGtwMuJ+nqgsx9ylkMiJ5JlK9bkj8AdvniIe5615wWQYkKHXe84XtSuc40G/tlrPu0A5/NlJvv8A==", + "engines": { + "node": "*" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -18568,6 +19366,32 @@ "tslib": "^2.0.3" } }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -18607,7 +19431,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -19060,6 +19883,11 @@ "postcss": "^8.4.31" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/stylus": { "version": "0.59.0", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", @@ -19264,7 +20092,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -19309,6 +20136,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", @@ -19613,6 +20445,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -19671,7 +20511,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "engines": { "node": ">=4" } @@ -19969,6 +20808,11 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/twitter-api-v2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.16.0.tgz", + "integrity": "sha512-e7wzWqx5oQZ9IUc2xt8JiJ84ioVGlmjmca1h0NwCKx2AFfKXDePmQa42gcBDe0qau9YaHsIpzO9z5SX1fmb+IQ==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -20109,6 +20953,18 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -20295,6 +21151,51 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.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", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -20586,6 +21487,11 @@ "defaults": "^1.0.3" } }, + "node_modules/webfontloader": { + "version": "1.6.28", + "resolved": "https://registry.npmjs.org/webfontloader/-/webfontloader-1.6.28.tgz", + "integrity": "sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -21066,6 +21972,11 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -21154,6 +22065,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -21180,7 +22099,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index a2507bba..e9f9a010 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "license": "MIT", "scripts": { - "dev": "concurrently \"stripe listen --forward-to localhost:3000/payment\" \"nx run-many --target=serve --projects=frontend,backend,workers,cron --parallel=4\"", + "dev": "concurrently \"stripe listen --forward-to localhost:3000/payment\" \"nx run-many --target=serve --projects=frontend,backend --parallel=4\"", "workers": "nx run workers:serve:development", "cron": "nx run cron:serve:development", "command": "nx run commands:build && nx run commands:command", @@ -12,27 +12,37 @@ }, "private": true, "dependencies": { + "@casl/ability": "^6.5.0", "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/microservices": "^10.3.1", "@nestjs/platform-express": "^10.0.2", "@nestjs/schedule": "^4.0.0", + "@novu/node": "^0.23.0", + "@novu/notification-center": "^0.23.0", "@prisma/client": "^5.8.1", "@swc/helpers": "~0.5.2", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.5", + "@types/lodash": "^4.14.202", + "@types/md5": "^2.3.5", "@types/stripe": "^8.0.417", "axios": "^1.0.0", "bcrypt": "^5.1.1", "bullmq": "^5.1.5", + "chart.js": "^4.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "clsx": "^2.1.0", "cookie-parser": "^1.4.6", + "dayjs": "^1.11.10", "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", + "md5": "^2.3.0", "nestjs-command": "^3.1.4", "next": "13.4.4", + "prisma-paginate": "^5.2.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.49.3", @@ -41,8 +51,10 @@ "redis": "^4.6.12", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", + "simple-statistics": "^7.8.3", "stripe": "^14.14.0", "tslib": "^2.3.0", + "twitter-api-v2": "^1.16.0", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/tsconfig.base.json b/tsconfig.base.json index a99bbf0e..a9f957f4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -9,6 +9,8 @@ "strictPropertyInitialization": false, "experimentalDecorators": true, "noPropertyAccessFromIndexSignature": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "importHelpers": true, "target": "es2015", "module": "esnext", @@ -20,6 +22,7 @@ "@gitroom/backend/*": ["apps/backend/src/*"], "@gitroom/frontend/*": ["apps/frontend/src/*"], "@gitroom/nestjs-libraries/*": ["libraries/nestjs-libraries/src/*"], + "@gitroom/react/*": ["libraries/react-shared-libraries/src/*"], "@gitroom/helpers/*": ["libraries/helpers/src/*"], "@gitroom/workers/*": ["apps/workers/src/*"], "@gitroom/cron/*": ["apps/cron/src/*"]