feat: gitroom features
This commit is contained in:
parent
00809fa072
commit
bfa36a0dcf
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Request>
|
||||
) {
|
||||
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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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<boolean> {
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
if (request.path.indexOf('/auth') > -1 || request.path.indexOf('/stripe') > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const policyHandlers =
|
||||
this._reflector.get<AbilityPolicy[]>(
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<[AuthorizationActions, Sections]>>(Ability as AbilityClass<AppAbility>);
|
||||
|
||||
// const options = await this.getPackageOptions(orgId);
|
||||
|
||||
return build({
|
||||
detectSubjectType: (item) =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
item.constructor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <login>',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 * * *')
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_159_1936)">
|
||||
<path d="M4.16699 9.99992L8.33366 14.1666L16.667 5.83325" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_159_1936">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 386 B |
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_153_22051)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3558 0C9.09958 0 0 9.16667 0 20.5071C0 29.5721 5.83042 37.2454 13.9187 39.9612C14.93 40.1654 15.3004 39.52 15.3004 38.9771C15.3004 38.5017 15.2671 36.8721 15.2671 35.1742C9.60458 36.3967 8.42542 32.7296 8.42542 32.7296C7.51542 30.3529 6.16708 29.7421 6.16708 29.7421C4.31375 28.4858 6.30208 28.4858 6.30208 28.4858C8.35792 28.6217 9.43667 30.5908 9.43667 30.5908C11.2563 33.7142 14.1883 32.8317 15.3679 32.2883C15.5363 30.9642 16.0758 30.0475 16.6488 29.5383C12.1325 29.0629 7.38083 27.2975 7.38083 19.4204C7.38083 17.1796 8.18917 15.3462 9.47 13.9204C9.26792 13.4113 8.56 11.3058 9.6725 8.48792C9.6725 8.48792 11.3913 7.94458 15.2667 10.5929C16.9259 10.144 18.637 9.91567 20.3558 9.91375C22.0746 9.91375 23.8267 10.1517 25.4446 10.5929C29.3204 7.94458 31.0392 8.48792 31.0392 8.48792C32.1517 11.3058 31.4433 13.4113 31.2412 13.9204C32.5558 15.3462 33.3308 17.1796 33.3308 19.4204C33.3308 27.2975 28.5792 29.0287 24.0292 29.5383C24.7708 30.1833 25.4108 31.4054 25.4108 33.3408C25.4108 36.0908 25.3775 38.2979 25.3775 38.9767C25.3775 39.52 25.7483 40.1654 26.7592 39.9617C34.8475 37.245 40.6779 29.5721 40.6779 20.5071C40.7113 9.16667 31.5783 0 20.3558 0Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_153_22051">
|
||||
<rect width="40.8333" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,48 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_52_3481)">
|
||||
<rect width="40" height="40" rx="20" fill="url(#paint0_linear_52_3481)"/>
|
||||
<g filter="url(#filter0_f_52_3481)">
|
||||
<ellipse cx="20.0742" cy="11.582" rx="20" ry="18" fill="url(#paint1_linear_52_3481)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_52_3481)">
|
||||
<path d="M29.9832 18.7232L25.7551 22.4132L27.0217 27.9069C27.0887 28.1941 27.0696 28.4947 26.9667 28.7711C26.8638 29.0475 26.6817 29.2874 26.4432 29.4609C26.2047 29.6344 25.9204 29.7337 25.6257 29.7464C25.3311 29.7592 25.0393 29.6848 24.7867 29.5326L19.9951 26.6263L15.2138 29.5326C14.9613 29.6848 14.6694 29.7592 14.3748 29.7464C14.0801 29.7337 13.7958 29.6344 13.5573 29.4609C13.3188 29.2874 13.1367 29.0475 13.0338 28.7711C12.931 28.4947 12.9118 28.1941 12.9788 27.9069L14.2435 22.4188L10.0145 18.7232C9.79079 18.5303 9.62905 18.2756 9.54953 17.9911C9.47 17.7067 9.47624 17.405 9.56745 17.1241C9.65866 16.8432 9.83079 16.5954 10.0622 16.4119C10.2937 16.2284 10.5742 16.1173 10.8685 16.0926L16.4429 15.6097L18.6188 10.4197C18.7325 10.1474 18.9241 9.9148 19.1697 9.75117C19.4153 9.58755 19.7038 9.50024 19.9988 9.50024C20.2939 9.50024 20.5824 9.58755 20.828 9.75117C21.0736 9.9148 21.2652 10.1474 21.3788 10.4197L23.5613 15.6097L29.1338 16.0926C29.4282 16.1173 29.7087 16.2284 29.9401 16.4119C30.1716 16.5954 30.3437 16.8432 30.4349 17.1241C30.5261 17.405 30.5324 17.7067 30.4529 17.9911C30.3733 18.2756 30.2116 18.5303 29.9879 18.7232H29.9832Z" fill="url(#paint2_linear_52_3481)"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="39" height="39" rx="19.5" stroke="url(#paint3_linear_52_3481)"/>
|
||||
<defs>
|
||||
<filter id="filter0_f_52_3481" x="-14.9258" y="-21.418" width="70" height="66" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="7.5" result="effect1_foregroundBlur_52_3481"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_52_3481" x="1.66958" y="6.89206" width="36.6633" height="35.8967" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="5.21637"/>
|
||||
<feGaussianBlur stdDeviation="3.91228"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0445331 0 0 0 0 0.305029 0 0 0 0 0.0862125 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_52_3481"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_52_3481" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_52_3481" x1="20" y1="-6.5" x2="32" y2="62.25" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#29357B"/>
|
||||
<stop offset="1" stop-color="#3762FB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_52_3481" x1="20.0742" y1="0.582031" x2="20.0742" y2="29.582" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_52_3481" x1="20.0012" y1="9.50024" x2="20.0012" y2="29.7478" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#D69F84"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_52_3481" x1="18.5371" y1="-21.709" x2="20" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_52_3481">
|
||||
<rect width="40" height="40" rx="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" rx="18" fill="#22394F"/>
|
||||
<path d="M22.375 14.9977V14.5625C22.375 12.6031 19.4195 11.125 15.5 11.125C11.5805 11.125 8.625 12.6031 8.625 14.5625V17.6875C8.625 19.3195 10.6758 20.6164 13.625 21.0047V21.4375C13.625 23.3969 16.5805 24.875 20.5 24.875C24.4195 24.875 27.375 23.3969 27.375 21.4375V18.3125C27.375 16.6953 25.3891 15.3969 22.375 14.9977ZM26.125 18.3125C26.125 19.3453 23.7195 20.5 20.5 20.5C20.2086 20.5 19.9195 20.4898 19.6344 20.4711C21.3195 19.857 22.375 18.8594 22.375 17.6875V16.2609C24.7086 16.6086 26.125 17.5523 26.125 18.3125ZM13.625 19.7383V17.8797C14.2467 17.9607 14.873 18.0009 15.5 18C16.127 18.0009 16.7533 17.9607 17.375 17.8797V19.7383C16.7542 19.83 16.1275 19.8757 15.5 19.875C14.8725 19.8757 14.2458 19.83 13.625 19.7383ZM21.125 16.5883V17.6875C21.125 18.343 20.1555 19.0469 18.625 19.4742V17.6484C19.6336 17.4039 20.4875 17.0398 21.125 16.5883ZM15.5 12.375C18.7195 12.375 21.125 13.5297 21.125 14.5625C21.125 15.5953 18.7195 16.75 15.5 16.75C12.2805 16.75 9.875 15.5953 9.875 14.5625C9.875 13.5297 12.2805 12.375 15.5 12.375ZM9.875 17.6875V16.5883C10.5125 17.0398 11.3664 17.4039 12.375 17.6484V19.4742C10.8445 19.0469 9.875 18.343 9.875 17.6875ZM14.875 21.4375V21.1117C15.0805 21.1195 15.2883 21.125 15.5 21.125C15.8031 21.125 16.0992 21.1148 16.3898 21.0977C16.7127 21.2132 17.0416 21.3113 17.375 21.3914V23.2242C15.8445 22.7969 14.875 22.093 14.875 21.4375ZM18.625 23.4883V21.625C19.2465 21.7085 19.8729 21.7503 20.5 21.75C21.127 21.7509 21.7533 21.7107 22.375 21.6297V23.4883C21.1316 23.6706 19.8684 23.6706 18.625 23.4883ZM23.625 23.2242V21.3984C24.6336 21.1539 25.4875 20.7898 26.125 20.3383V21.4375C26.125 22.093 25.1555 22.7969 23.625 23.2242Z" fill="#8B90FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -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</>
|
||||
<AnalyticsComponent list={analytics} trending={trending} stars={stars.stars} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<SettingsComponent github={github} organizations={organizations} />
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<html>
|
||||
<body className="overflow-hidden">
|
||||
<body className={chakra.className}>
|
||||
<LayoutContext>
|
||||
{children}
|
||||
</LayoutContext>
|
||||
|
|
|
|||
|
|
@ -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<StarsAndForksInterface> = (props) => {
|
||||
return (
|
||||
<div className="flex gap-[24px] flex-1">
|
||||
<div className="flex flex-col gap-[24px] flex-1">
|
||||
<StarsAndForks {...props} />
|
||||
<div className="flex flex-1 flex-col gap-[15px] min-h-[426px]">
|
||||
<h2 className="text-[24px]">Stars per day</h2>
|
||||
<div className="flex-1 bg-secondary">
|
||||
<StarsTableComponent stars={props.stars} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[318px] bg-third mt-[-44px] p-[16px]">
|
||||
<h2 className="text-[20px]">News Feed</h2>
|
||||
<div className="my-[30px] flex h-[32px]">
|
||||
<div className="flex-1 bg-forth flex justify-center items-center">
|
||||
Global
|
||||
</div>
|
||||
<div className="flex-1 bg-primary flex justify-center items-center">
|
||||
My Feed
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-full flex-col justify-start items-start gap-4 inline-flex">
|
||||
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
|
||||
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
|
||||
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
|
||||
<div className="justify-center items-center gap-1 inline-flex">
|
||||
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
|
||||
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
|
||||
</div>
|
||||
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
|
||||
<div className="self-stretch justify-start items-center gap-1 inline-flex">
|
||||
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
|
||||
<div className="w-4 h-4 relative"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
|
||||
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
|
||||
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
|
||||
<div className="justify-center items-center gap-1 inline-flex">
|
||||
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
|
||||
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
|
||||
</div>
|
||||
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
|
||||
<div className="self-stretch justify-start items-center gap-1 inline-flex">
|
||||
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
|
||||
<div className="w-4 h-4 relative"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
|
||||
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
|
||||
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
|
||||
<div className="justify-center items-center gap-1 inline-flex">
|
||||
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
|
||||
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
|
||||
</div>
|
||||
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
|
||||
<div className="self-stretch justify-start items-center gap-1 inline-flex">
|
||||
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
|
||||
<div className="w-4 h-4 relative"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
|
||||
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
|
||||
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
|
||||
<div className="justify-center items-center gap-1 inline-flex">
|
||||
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
|
||||
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
|
||||
</div>
|
||||
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
|
||||
<div className="self-stretch justify-start items-center gap-1 inline-flex">
|
||||
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
|
||||
<div className="w-4 h-4 relative"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
|
||||
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
|
||||
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
|
||||
<div className="justify-center items-center gap-1 inline-flex">
|
||||
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
|
||||
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
|
||||
</div>
|
||||
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
|
||||
<div className="self-stretch justify-start items-center gap-1 inline-flex">
|
||||
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
|
||||
<div className="w-4 h-4 relative"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<any>(null);
|
||||
const chart = useRef<null | DrawChart>(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 <canvas className="w-full h-full" ref={ref} />
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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<StarsAndForksInterface> = (props) => {
|
||||
const {list} = props;
|
||||
console.log(list);
|
||||
return (
|
||||
<>
|
||||
{list.map(item => (
|
||||
<div className="flex gap-[24px] h-[272px]" key={item.login}>
|
||||
{[1,2].map(p => (
|
||||
<div key={p} className="flex-1 bg-secondary py-[10px] px-[16px] flex flex-col">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div>
|
||||
<Image src="/icons/star-circle.svg" alt="Stars" width={40} height={40}/>
|
||||
</div>
|
||||
<div className="text-[20px]">
|
||||
{item.login.split('/')[1].split('').map(((char, index) => index === 0 ? char.toUpperCase() : char)).join('')} {p === 1 ? 'Stars' : 'Forks'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute w-full h-full left-0 top-0">
|
||||
{item.stars.length ? <Chart list={item.stars}/> : <div className="w-full h-full flex items-center justify-center text-3xl">Processing stars...</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[50px] leading-[60px]">
|
||||
{item?.stars[item.stars.length - 1]?.totalStars}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-[24px]">
|
||||
{[0, 1].map( p => (
|
||||
<div key={p} className="flex-1 bg-secondary py-[24px] px-[16px] gap-[16px] flex flex-col">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div>
|
||||
<Image src="/icons/trending.svg" width={36} height={36} alt="Trending" />
|
||||
</div>
|
||||
<div className="text-[20px]">
|
||||
{p === 0 ? 'Last Github Trending' : 'Next Predicted GitHub Trending'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="w-[2px] h-[30px] bg-[#8B90FF] mr-[16px]"></div>
|
||||
<div className="text-[24px] flex-1">
|
||||
<UtcToLocalDateRender date={p === 0 ? props.trending.last : props.trending.predictions} format="dddd"/>
|
||||
</div>
|
||||
<div className={clsx("text-[24px]", p === 0 ? 'text-[#B7C1FF]' : 'text-[#FFAC30]')}>
|
||||
<UtcToLocalDateRender date={p === 0 ? props.trending.last : props.trending.predictions} format="DD MMM YYYY"/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rounded-full bg-[#576A9A] w-[5px] h-[5px] mx-[8px]"/>
|
||||
</div>
|
||||
<div className={clsx("text-[24px]", p === 0 ? 'text-[#B7C1FF]' : 'text-[#FFAC30]')}>
|
||||
<UtcToLocalDateRender date={p === 0 ? props.trending.last : props.trending.predictions} format="HH:mm"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>))}
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<table className="table1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Repository</th>
|
||||
<th>Date</th>
|
||||
<th>Total</th>
|
||||
<th>Stars</th>
|
||||
<th>Media</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stars.map(p => (<tr key={p.date}>
|
||||
<td>{p.login}</td>
|
||||
<td><UtcToLocalDateRender date={p.date} format="DD/MM/YYYY" /></td>
|
||||
<td>{p.totalStars}</td>
|
||||
<td>{p.stars}</td>
|
||||
<td>Media</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ export function Register() {
|
|||
|
||||
const fetchData = useFetch();
|
||||
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
fetchData('/auth/register', {
|
||||
const onSubmit: SubmitHandler<Inputs> = async (data) => {
|
||||
await fetchData('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({...data, provider: 'LOCAL'})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 <LayoutContextInner children={params.children} />
|
||||
}
|
||||
|
||||
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 || <></>}
|
||||
</FetchWrapperComponent>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<ContextWrapper user={user}>
|
||||
<div className="min-w-[100vw] min-h-[100vh] bg-primary py-[14px] px-[12px] text-white flex">
|
||||
<div className="w-[216px] p-2 gap-10 flex flex-col">
|
||||
<div>
|
||||
Logo
|
||||
<div className="min-h-[100vh] bg-primary px-[12px] text-white flex flex-col">
|
||||
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<div className="text-2xl">
|
||||
Gitroom
|
||||
</div>
|
||||
<TopMenu />
|
||||
<div>
|
||||
<NotificationComponent />
|
||||
</div>
|
||||
<LeftMenu />
|
||||
</div>
|
||||
<div className="flex-1 flex">
|
||||
<div className="bg-secondary flex-1 rounded-3xl px-[24px] py-[28px]">
|
||||
<div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col">
|
||||
<Title />
|
||||
{children}
|
||||
<div className="flex flex-1 flex-col">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -52,6 +52,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy {
|
|||
},
|
||||
{
|
||||
...this.options,
|
||||
...handler?.extras
|
||||
},
|
||||
);
|
||||
this.workers.set(pattern, worker);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import {IsIn} from "class-validator";
|
||||
|
||||
export class BillingSubscribeDto {
|
||||
@IsIn(['MONTHLY', 'YEARLY'])
|
||||
period: 'MONTHLY' | 'YEARLY';
|
||||
|
||||
@IsIn(['BASIC', 'PRO'])
|
||||
billing: 'BASIC' | 'PRO';
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import {IsDefined, IsString} from "class-validator";
|
||||
|
||||
export class ConnectIntegrationDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
state: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
code: string;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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)!;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
@ -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)} />
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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)}</>;
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
14
package.json
14
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": {
|
||||
|
|
|
|||
|
|
@ -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/*"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue