feat: analytics
This commit is contained in:
parent
4a5e93c356
commit
01161692ba
|
|
@ -1,42 +1,83 @@
|
|||
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 {StarsListDto} from "@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto";
|
||||
import {ApiTags} from "@nestjs/swagger";
|
||||
import { Body, Controller, Get, Inject, Param, Post, Query } 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 { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
|
||||
@ApiTags('Analytics')
|
||||
@Controller('/analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private _starsService: StarsService
|
||||
) {
|
||||
}
|
||||
@Get('/')
|
||||
async getStars(
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._starsService.getStars(org.id);
|
||||
constructor(
|
||||
private _starsService: StarsService,
|
||||
private _integrationService: IntegrationService,
|
||||
private _integrationManager: IntegrationManager
|
||||
) {}
|
||||
@Get('/')
|
||||
async getStars(@GetOrgFromRequest() org: Organization) {
|
||||
return this._starsService.getStars(org.id);
|
||||
}
|
||||
|
||||
@Get('/trending')
|
||||
async getTrending() {
|
||||
const todayTrending = dayjs(dayjs().format('YYYY-MM-DDT12:00:00'));
|
||||
const last = todayTrending.isAfter(dayjs())
|
||||
? todayTrending.subtract(1, 'day')
|
||||
: todayTrending;
|
||||
const nextTrending = last.add(1, 'day');
|
||||
|
||||
return {
|
||||
last: last.format('YYYY-MM-DD HH:mm:ss'),
|
||||
predictions: nextTrending.format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/stars')
|
||||
async getStarsFilter(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() starsFilter: StarsListDto
|
||||
) {
|
||||
return {
|
||||
stars: await this._starsService.getStarsFilter(org.id, starsFilter),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/:integration')
|
||||
async getIntegration(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('integration') integration: string,
|
||||
@Query('date') date: string
|
||||
) {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
integration
|
||||
);
|
||||
|
||||
if (!getIntegration) {
|
||||
throw new Error('Invalid integration');
|
||||
}
|
||||
|
||||
@Get('/trending')
|
||||
async getTrending() {
|
||||
const todayTrending = dayjs(dayjs().format('YYYY-MM-DDT12:00:00'));
|
||||
const last = todayTrending.isAfter(dayjs()) ? todayTrending.subtract(1, 'day') : todayTrending;
|
||||
const nextTrending = last.add(1, 'day');
|
||||
if (getIntegration.type === 'social') {
|
||||
const integrationProvider = this._integrationManager.getSocialIntegration(
|
||||
getIntegration.providerIdentifier
|
||||
);
|
||||
|
||||
return {
|
||||
last: last.format('YYYY-MM-DD HH:mm:ss'),
|
||||
predictions: nextTrending.format('YYYY-MM-DD HH:mm:ss'),
|
||||
}
|
||||
}
|
||||
const getIntegrationData = await ioRedis.get(`integration:${org.id}:${integration}:${date}`);
|
||||
if (getIntegrationData) {
|
||||
return JSON.parse(getIntegrationData)
|
||||
}
|
||||
|
||||
@Post('/stars')
|
||||
async getStarsFilter(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() starsFilter: StarsListDto
|
||||
) {
|
||||
return {stars: await this._starsService.getStarsFilter(org.id, starsFilter)};
|
||||
if (integrationProvider.analytics) {
|
||||
const loadAnalytics = await integrationProvider.analytics(getIntegration.internalId, getIntegration.token, +date);
|
||||
await ioRedis.set(`integration:${org.id}:${integration}:${date}`, JSON.stringify(loadAnalytics), 'EX', !process.env.NODE_ENV || process.env.NODE_ENV === 'development' ? 1 : 3600);
|
||||
return loadAnalytics;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,8 @@ export class IntegrationsController {
|
|||
throw new Error('Invalid state');
|
||||
}
|
||||
|
||||
await ioRedis.del(`login:${body.state}`);
|
||||
|
||||
const integrationProvider =
|
||||
this._integrationManager.getSocialIntegration(integration);
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -2,16 +2,19 @@ import { isGeneral } from '@gitroom/react/helpers/is.general';
|
|||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import {AnalyticsComponent} from "@gitroom/frontend/components/analytics/analytics.component";
|
||||
import {Metadata} from "next";
|
||||
import { AnalyticsComponent } from '@gitroom/frontend/components/analytics/analytics.component';
|
||||
import { Metadata } from 'next';
|
||||
import { PlatformAnalytics } from '@gitroom/frontend/components/platform-analytics/platform.analytics';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${isGeneral() ? 'Postiz' : 'Gitroom'} Analytics`,
|
||||
description: '',
|
||||
}
|
||||
};
|
||||
|
||||
export default async function Index() {
|
||||
return (
|
||||
<AnalyticsComponent />
|
||||
<>
|
||||
{isGeneral() ? <PlatformAnalytics /> : <AnalyticsComponent />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
'use client';
|
||||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import DrawChart from 'chart.js/auto';
|
||||
import { TotalList } from '@gitroom/frontend/components/analytics/stars.and.forks.interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { chunk } from 'lodash';
|
||||
|
||||
function mergeDataPoints(data: TotalList[], numPoints: number): TotalList[] {
|
||||
const res = chunk(data, Math.ceil(data.length / numPoints));
|
||||
return res.map((row) => {
|
||||
return {
|
||||
date: `${row[0].date} - ${row?.at(-1)?.date}`,
|
||||
total: row.reduce((acc, curr) => acc + curr.total, 0),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const ChartSocial: FC<{ data: TotalList[] }> = (props) => {
|
||||
const { data } = props;
|
||||
const list = useMemo(() => {
|
||||
return mergeDataPoints(data, 7);
|
||||
}, [data]);
|
||||
|
||||
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, 'rgb(20,101,6)'); // 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,
|
||||
ticks: {
|
||||
stepSize: 10,
|
||||
maxTicksLimit: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
labels: list.map((row) => row.date),
|
||||
datasets: [
|
||||
{
|
||||
borderColor: '#fff',
|
||||
// @ts-ignore
|
||||
label: 'Total',
|
||||
backgroundColor: gradient,
|
||||
fill: true,
|
||||
// @ts-ignore
|
||||
data: list.map((row) => row.total),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
chart?.current?.destroy();
|
||||
};
|
||||
}, []);
|
||||
return <canvas className="w-full h-full" ref={ref} />;
|
||||
};
|
||||
|
|
@ -3,6 +3,11 @@ export interface StarsList {
|
|||
date: string;
|
||||
}
|
||||
|
||||
export interface TotalList {
|
||||
total: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface ForksList {
|
||||
totalForks: number;
|
||||
date: string;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,28 @@
|
|||
'use client';
|
||||
|
||||
export const Support = () => {
|
||||
if (!process.env.NEXT_PUBLIC_DISCORD_SUPPORT) return null
|
||||
if (!process.env.NEXT_PUBLIC_DISCORD_SUPPORT) return null;
|
||||
return (
|
||||
<div className="bg-[#612AD5] fixed right-[20px] bottom-[20px] z-[500] p-[20px] text-white rounded-[20px] cursor-pointer" onClick={() => window.open(process.env.NEXT_PUBLIC_DISCORD_SUPPORT)}>Discord Support</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className="bg-[#fff] w-[194px] h-[58px] fixed right-[20px] bottom-[20px] z-[500] text-[16px] text-[#0E0E0E] rounded-[30px] !rounded-br-[0] cursor-pointer flex justify-center items-center gap-[10px]"
|
||||
onClick={() => window.open(process.env.NEXT_PUBLIC_DISCORD_SUPPORT)}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
width="32"
|
||||
height="33"
|
||||
viewBox="0 0 32 33"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mb-[4px]"
|
||||
>
|
||||
<path
|
||||
d="M26.1303 11.347C24.3138 9.93899 22.134 9.23502 19.8331 9.11768L19.4697 9.4697C21.5284 9.93899 23.345 10.8776 25.0404 12.1683C22.9817 11.1123 20.6807 10.4084 18.2587 10.1737C17.5321 10.0563 16.9266 10.0563 16.2 10.0563C15.4734 10.0563 14.8679 10.0563 14.1413 10.1737C11.7193 10.4084 9.41833 11.1123 7.35963 12.1683C9.05501 10.8776 10.8716 9.93899 12.9303 9.4697L12.5669 9.11768C10.266 9.23502 8.08621 9.93899 6.26972 11.347C4.21101 15.1017 3.1211 19.3257 3 23.6669C4.81649 25.5443 7.35963 26.7177 10.0239 26.7177C10.0239 26.7177 10.8716 25.779 11.477 24.9576C9.90277 24.6057 8.44954 23.7843 7.48074 22.4937C8.32843 22.963 9.17611 23.4323 10.0239 23.7843C11.1138 24.2537 12.2037 24.4883 13.2936 24.723C14.2624 24.8403 15.2312 24.9576 16.2 24.9576C17.1688 24.9576 18.1376 24.8403 19.1064 24.723C20.1963 24.4883 21.2862 24.2537 22.3761 23.7843C23.2239 23.4323 24.0716 22.963 24.9193 22.4937C23.9505 23.7843 22.4972 24.6057 20.923 24.9576C21.5284 25.779 22.3761 26.7177 22.3761 26.7177C25.0404 26.7177 27.5835 25.5443 29.4 23.6669C29.2789 19.3257 28.189 15.1017 26.1303 11.347ZM12.2037 21.555C10.9927 21.555 9.90278 20.499 9.90278 19.2084C9.90278 17.9177 10.9927 16.8617 12.2037 16.8617C13.4147 16.8617 14.5046 17.9177 14.5046 19.2084C14.5046 20.499 13.4147 21.555 12.2037 21.555ZM20.1963 21.555C18.9853 21.555 17.8954 20.499 17.8954 19.2084C17.8954 17.9177 18.9853 16.8617 20.1963 16.8617C21.4073 16.8617 22.4972 17.9177 22.4972 19.2084C22.4972 20.499 21.4073 21.555 20.1963 21.555Z"
|
||||
fill="#0E0E0E"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Discord Support</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@ export const menuItems = [
|
|||
icon: 'launches',
|
||||
path: '/launches',
|
||||
},
|
||||
...(general
|
||||
? [
|
||||
{
|
||||
name: 'Analytics',
|
||||
icon: 'analytics',
|
||||
path: '/analytics',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!general
|
||||
? [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { orderBy } from 'lodash';
|
||||
import clsx from 'clsx';
|
||||
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
||||
import Image from 'next/image';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { RenderAnalytics } from '@gitroom/frontend/components/platform-analytics/render.analytics';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
|
||||
const allowedIntegrations = [
|
||||
'facebook',
|
||||
'instagram',
|
||||
'linkedin-page',
|
||||
'tiktok',
|
||||
'youtube',
|
||||
'pinterest'
|
||||
];
|
||||
|
||||
export const PlatformAnalytics = () => {
|
||||
const fetch = useFetch();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [key, setKey] = useState(7);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const int = (await (await fetch('/integrations/list')).json()).integrations;
|
||||
return int.filter((f: any) => allowedIntegrations.includes(f.identifier));
|
||||
}, []);
|
||||
|
||||
const { data } = useSWR('analytics-list', load, {
|
||||
fallbackData: [],
|
||||
});
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
data,
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
const currentIntegration = useMemo(() => {
|
||||
return sortedIntegrations[current];
|
||||
}, [current, sortedIntegrations]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!currentIntegration) {
|
||||
return [];
|
||||
}
|
||||
const arr = [];
|
||||
if (
|
||||
['facebook', 'instagram', 'linkedin-page', 'pinterest', 'youtube'].indexOf(
|
||||
currentIntegration.identifier
|
||||
) !== -1
|
||||
) {
|
||||
arr.push({
|
||||
key: 7,
|
||||
value: '7 Days',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
['facebook', 'instagram', 'linkedin-page', 'pinterest', 'youtube'].indexOf(
|
||||
currentIntegration.identifier
|
||||
) !== -1
|
||||
) {
|
||||
arr.push({
|
||||
key: 30,
|
||||
value: '30 Days',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
['facebook', 'linkedin-page', 'pinterest', 'youtube'].indexOf(currentIntegration.identifier) !==
|
||||
-1
|
||||
) {
|
||||
arr.push({
|
||||
key: 90,
|
||||
value: '90 Days',
|
||||
});
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [currentIntegration]);
|
||||
|
||||
const keys = useMemo(() => {
|
||||
if (!currentIntegration) {
|
||||
return 7;
|
||||
}
|
||||
if (options.find((p) => p.key === key)) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return options[0]?.key;
|
||||
}, [key, currentIntegration]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-[30px] flex-1">
|
||||
<div className="p-[16px] bg-[#080B14] overflow-hidden flex w-[220px]">
|
||||
<div className="flex gap-[16px] flex-col overflow-hidden">
|
||||
<div className="text-[20px] mb-[8px]">Channels</div>
|
||||
{sortedIntegrations.map((integration, index) => (
|
||||
<div
|
||||
key={integration.id}
|
||||
onClick={() => setCurrent(index)}
|
||||
className={clsx(
|
||||
'flex gap-[8px] items-center',
|
||||
currentIntegration.id !== integration.id &&
|
||||
'opacity-20 hover:opacity-100 cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer">
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-black/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<div className="max-w-[200px]">
|
||||
<Select
|
||||
className="bg-[#0A0B14] !border-0"
|
||||
label=""
|
||||
name="date"
|
||||
disableForm={true}
|
||||
hideErrors={true}
|
||||
onChange={(e) => setKey(+e.target.value)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.key} value={option.key}>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{!!keys && !!currentIntegration && (
|
||||
<RenderAnalytics integration={currentIntegration} date={keys} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Integration } from '@prisma/client';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { ChartSocial } from '@gitroom/frontend/components/analytics/chart-social';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
|
||||
export const RenderAnalytics: FC<{ integration: Integration; date: number }> = (
|
||||
props
|
||||
) => {
|
||||
const { integration, date } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetch = useFetch();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const load = (
|
||||
await fetch(`/analytics/${integration.id}?date=${date}`)
|
||||
).json();
|
||||
setLoading(false);
|
||||
return load;
|
||||
}, [integration, date]);
|
||||
|
||||
const { data } = useSWR(`/analytics-${integration?.id}-${date}`, load, {
|
||||
refreshInterval: 0,
|
||||
refreshWhenHidden: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
|
||||
const total = useMemo(() => {
|
||||
return data?.map(
|
||||
(p: any) => {
|
||||
const value = (p?.data.reduce((acc: number, curr: any) => acc + curr.total, 0) || 0) /
|
||||
(p.average ? p.data.length : 1);
|
||||
|
||||
if (p.average) {
|
||||
return value.toFixed(2) + '%';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<LoadingComponent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-[20px]">
|
||||
{data?.map((p: any, index: number) => (
|
||||
<div key={`pl-${index}`} className="flex">
|
||||
<div className="flex-1 bg-secondary py-[10px] px-[16px] gap-[10px] flex flex-col">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div className="text-[20px]">{p.label}</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-[156px] relative">
|
||||
<ChartSocial {...p} key={`p-${index}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[50px] leading-[60px]">{total[index]}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -87,12 +87,6 @@ export async function middleware(request: NextRequest) {
|
|||
return redirect;
|
||||
}
|
||||
|
||||
if (isGeneral() && (nextUrl.pathname.indexOf('/analytics') > -1 || nextUrl.pathname.indexOf('/settings') > -1)) {
|
||||
return NextResponse.redirect(
|
||||
new URL('/launches', nextUrl.href)
|
||||
);
|
||||
}
|
||||
|
||||
if (nextUrl.pathname === '/') {
|
||||
return NextResponse.redirect(
|
||||
new URL(isGeneral() ? '/launches' : `/analytics`, nextUrl.href)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { DribbbleProvider } from '@gitroom/nestjs-libraries/integrations/social/
|
|||
import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider';
|
||||
|
||||
const socialIntegrationList = [
|
||||
new XProvider(),
|
||||
...(process.env.IS_GENERAL !== 'true' ? [new XProvider()] : []),
|
||||
new LinkedinProvider(),
|
||||
new LinkedinPageProvider(),
|
||||
new RedditProvider(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
|
|
@ -14,7 +15,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
|
||||
export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'dribbble';
|
||||
name = 'Dribbbble';
|
||||
name = 'Dribbble';
|
||||
isBetweenSteps = false;
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
|
|
@ -67,10 +68,12 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
|||
})
|
||||
).json();
|
||||
|
||||
return teams?.map((team: any) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
})) || [];
|
||||
return (
|
||||
teams?.map((team: any) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
async generateAuthUrl(refresh?: string) {
|
||||
|
|
@ -268,4 +271,12 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
|
|
@ -32,7 +33,9 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
'https://www.facebook.com/v20.0/dialog/oauth' +
|
||||
`?client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook${refresh ? `?refresh=${refresh}` : ''}`
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook${
|
||||
refresh ? `?refresh=${refresh}` : ''
|
||||
}`
|
||||
)}` +
|
||||
`&state=${state}` +
|
||||
'&scope=pages_show_list,business_management,pages_manage_posts,pages_manage_engagement,pages_read_engagement',
|
||||
|
|
@ -259,4 +262,37 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
...postsArray,
|
||||
];
|
||||
}
|
||||
|
||||
async analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
|
||||
const { data } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/insights?metric=page_impressions_unique,page_posts_impressions_unique,page_post_engagements,page_daily_follows,page_video_views&access_token=${accessToken}&period=day&since=${since}&until=${until}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return data.map((d: any) => ({
|
||||
label:
|
||||
d.name === 'page_impressions_unique'
|
||||
? 'Page Impressions'
|
||||
: d.name === 'page_post_engagements'
|
||||
? 'Posts Engagement'
|
||||
: d.name === 'page_daily_follows'
|
||||
? 'Page followers'
|
||||
: d.name === 'page_video_views'
|
||||
? 'Videos views'
|
||||
: 'Posts Impressions',
|
||||
percentageChange: 5,
|
||||
data: d.values.map((v: any) => ({
|
||||
total: v.value,
|
||||
date: dayjs(v.end_time).format('YYYY-MM-DD'),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
|
|
@ -8,8 +9,12 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { chunk } from 'lodash';
|
||||
|
||||
export class InstagramProvider extends SocialAbstract implements SocialProvider {
|
||||
export class InstagramProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
{
|
||||
identifier = 'instagram';
|
||||
name = 'Instagram';
|
||||
isBetweenSteps = true;
|
||||
|
|
@ -39,7 +44,7 @@ export class InstagramProvider extends SocialAbstract implements SocialProvider
|
|||
)}` +
|
||||
`&state=${state}` +
|
||||
`&scope=${encodeURIComponent(
|
||||
'instagram_basic,pages_show_list,pages_read_engagement,business_management,instagram_content_publish,instagram_manage_comments'
|
||||
'instagram_basic,pages_show_list,pages_read_engagement,business_management,instagram_content_publish,instagram_manage_comments,instagram_manage_insights'
|
||||
)}`,
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
|
|
@ -88,7 +93,9 @@ export class InstagramProvider extends SocialAbstract implements SocialProvider
|
|||
).json();
|
||||
|
||||
if (params.refresh) {
|
||||
const findPage = (await this.pages(access_token)).find(p => p.id === params.refresh);
|
||||
const findPage = (await this.pages(access_token)).find(
|
||||
(p) => p.id === params.refresh
|
||||
);
|
||||
const information = await this.fetchPageInformation(access_token, {
|
||||
id: params.refresh,
|
||||
pageId: findPage?.pageId!,
|
||||
|
|
@ -319,4 +326,30 @@ export class InstagramProvider extends SocialAbstract implements SocialProvider
|
|||
|
||||
return arr;
|
||||
}
|
||||
|
||||
async analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
|
||||
const { data, ...all } = await (
|
||||
await fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/insights?metric=follower_count,impressions,reach,profile_views&access_token=${accessToken}&period=day&since=${since}&until=${until}`
|
||||
)
|
||||
).json();
|
||||
|
||||
console.log(all);
|
||||
|
||||
return data?.map((d: any) => ({
|
||||
label: d.title,
|
||||
percentageChange: 5,
|
||||
data: d.values.map((v: any) => ({
|
||||
total: v.value,
|
||||
date: dayjs(v.end_time).format('YYYY-MM-DD'),
|
||||
})),
|
||||
})) || [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
|
|
@ -6,6 +7,9 @@ import {
|
|||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
|
||||
import { number, string } from 'yup';
|
||||
import dayjs from 'dayjs';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
export class LinkedinPageProvider
|
||||
extends LinkedinProvider
|
||||
|
|
@ -121,7 +125,8 @@ export class LinkedinPageProvider
|
|||
id: data.id,
|
||||
name: data.localizedName,
|
||||
access_token: accessToken,
|
||||
picture: data?.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0].identifier,
|
||||
picture:
|
||||
data?.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0].identifier,
|
||||
username: data.vanityName,
|
||||
};
|
||||
}
|
||||
|
|
@ -195,4 +200,332 @@ export class LinkedinPageProvider
|
|||
): Promise<PostResponse[]> {
|
||||
return super.post(id, accessToken, postDetails, 'company');
|
||||
}
|
||||
|
||||
async analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const endDate = dayjs().unix() * 1000;
|
||||
const startDate = dayjs().subtract(date, 'days').unix() * 1000;
|
||||
|
||||
const { elements }: { elements: Root[]; paging: any } = await (
|
||||
await fetch(
|
||||
`https://api.linkedin.com/rest/organizationPageStatistics?q=organization&organization=${encodeURIComponent(
|
||||
`urn:li:organization:${id}`
|
||||
)}&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Linkedin-Version': '202405',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { elements: elements2 }: { elements: Root[]; paging: any } = await (
|
||||
await fetch(
|
||||
`https://api.linkedin.com/rest/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(
|
||||
`urn:li:organization:${id}`
|
||||
)}&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Linkedin-Version': '202405',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { elements: elements3 }: { elements: Root[]; paging: any } = await (
|
||||
await fetch(
|
||||
`https://api.linkedin.com/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(
|
||||
`urn:li:organization:${id}`
|
||||
)}&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Linkedin-Version': '202405',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const analytics = [...elements2, ...elements, ...elements3].reduce(
|
||||
(all, current) => {
|
||||
if (
|
||||
typeof current?.totalPageStatistics?.views?.allPageViews
|
||||
?.pageViews !== 'undefined'
|
||||
) {
|
||||
all['Page Views'].push({
|
||||
total: current.totalPageStatistics.views.allPageViews.pageViews,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
typeof current?.followerGains?.organicFollowerGain !== 'undefined'
|
||||
) {
|
||||
all['Organic Followers'].push({
|
||||
total: current?.followerGains?.organicFollowerGain,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof current?.followerGains?.paidFollowerGain !== 'undefined') {
|
||||
all['Paid Followers'].push({
|
||||
total: current?.followerGains?.paidFollowerGain,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof current?.totalShareStatistics !== 'undefined') {
|
||||
all['Clicks'].push({
|
||||
total: current?.totalShareStatistics.clickCount,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
|
||||
all['Shares'].push({
|
||||
total: current?.totalShareStatistics.shareCount,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
|
||||
all['Engagement'].push({
|
||||
total: current?.totalShareStatistics.engagement,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
|
||||
all['Comments'].push({
|
||||
total: current?.totalShareStatistics.commentCount,
|
||||
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
{
|
||||
'Page Views': [] as any[],
|
||||
Clicks: [] as any[],
|
||||
Shares: [] as any[],
|
||||
Engagement: [] as any[],
|
||||
Comments: [] as any[],
|
||||
'Organic Followers': [] as any[],
|
||||
'Paid Followers': [] as any[],
|
||||
}
|
||||
);
|
||||
|
||||
return Object.keys(analytics).map((key) => ({
|
||||
label: key,
|
||||
data: analytics[
|
||||
key as 'Page Views' | 'Organic Followers' | 'Paid Followers'
|
||||
],
|
||||
percentageChange: 5,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export interface Root {
|
||||
pageStatisticsByIndustryV2: any[];
|
||||
pageStatisticsBySeniority: any[];
|
||||
organization: string;
|
||||
pageStatisticsByGeoCountry: any[];
|
||||
pageStatisticsByTargetedContent: any[];
|
||||
totalPageStatistics: TotalPageStatistics;
|
||||
pageStatisticsByStaffCountRange: any[];
|
||||
pageStatisticsByFunction: any[];
|
||||
pageStatisticsByGeo: any[];
|
||||
followerGains: { organicFollowerGain: number; paidFollowerGain: number };
|
||||
timeRange: TimeRange;
|
||||
totalShareStatistics: {
|
||||
uniqueImpressionsCount: number;
|
||||
shareCount: number;
|
||||
engagement: number;
|
||||
clickCount: number;
|
||||
likeCount: number;
|
||||
impressionCount: number;
|
||||
commentCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TotalPageStatistics {
|
||||
clicks: Clicks;
|
||||
views: Views;
|
||||
}
|
||||
|
||||
export interface Clicks {
|
||||
mobileCustomButtonClickCounts: any[];
|
||||
desktopCustomButtonClickCounts: any[];
|
||||
}
|
||||
|
||||
export interface Views {
|
||||
mobileProductsPageViews: MobileProductsPageViews;
|
||||
allDesktopPageViews: AllDesktopPageViews;
|
||||
insightsPageViews: InsightsPageViews;
|
||||
mobileAboutPageViews: MobileAboutPageViews;
|
||||
allMobilePageViews: AllMobilePageViews;
|
||||
productsPageViews: ProductsPageViews;
|
||||
desktopProductsPageViews: DesktopProductsPageViews;
|
||||
jobsPageViews: JobsPageViews;
|
||||
peoplePageViews: PeoplePageViews;
|
||||
overviewPageViews: OverviewPageViews;
|
||||
mobileOverviewPageViews: MobileOverviewPageViews;
|
||||
lifeAtPageViews: LifeAtPageViews;
|
||||
desktopOverviewPageViews: DesktopOverviewPageViews;
|
||||
mobileCareersPageViews: MobileCareersPageViews;
|
||||
allPageViews: AllPageViews;
|
||||
careersPageViews: CareersPageViews;
|
||||
mobileJobsPageViews: MobileJobsPageViews;
|
||||
mobileLifeAtPageViews: MobileLifeAtPageViews;
|
||||
desktopJobsPageViews: DesktopJobsPageViews;
|
||||
desktopPeoplePageViews: DesktopPeoplePageViews;
|
||||
aboutPageViews: AboutPageViews;
|
||||
desktopAboutPageViews: DesktopAboutPageViews;
|
||||
mobilePeoplePageViews: MobilePeoplePageViews;
|
||||
desktopCareersPageViews: DesktopCareersPageViews;
|
||||
desktopInsightsPageViews: DesktopInsightsPageViews;
|
||||
desktopLifeAtPageViews: DesktopLifeAtPageViews;
|
||||
mobileInsightsPageViews: MobileInsightsPageViews;
|
||||
}
|
||||
|
||||
export interface MobileProductsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface AllDesktopPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface InsightsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobileAboutPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface AllMobilePageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface ProductsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopProductsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface JobsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface PeoplePageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface OverviewPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobileOverviewPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface LifeAtPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopOverviewPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobileCareersPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface AllPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface CareersPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobileJobsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobileLifeAtPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopJobsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopPeoplePageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface AboutPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopAboutPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobilePeoplePageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopCareersPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopInsightsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface DesktopLifeAtPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface MobileInsightsPageViews {
|
||||
pageViews: number;
|
||||
uniquePageViews: number;
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
AnalyticsData, AuthTokenDetails, PostDetails, PostResponse, SocialProvider
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import sharp from 'sharp';
|
||||
|
|
@ -10,6 +7,7 @@ import { lookup } from 'mime-types';
|
|||
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
|
||||
import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { number, string } from 'yup';
|
||||
|
||||
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
|
|
@ -10,8 +11,12 @@ import axios from 'axios';
|
|||
import FormData from 'form-data';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export class PinterestProvider extends SocialAbstract implements SocialProvider {
|
||||
export class PinterestProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
{
|
||||
identifier = 'pinterest';
|
||||
name = 'Pinterest';
|
||||
isBetweenSteps = false;
|
||||
|
|
@ -181,15 +186,12 @@ export class PinterestProvider extends SocialAbstract implements SocialProvider
|
|||
while (statusCode !== 'succeeded') {
|
||||
console.log('trying');
|
||||
const mediafile = await (
|
||||
await this.fetch(
|
||||
'https://api.pinterest.com/v5/media/' + media_id,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
await this.fetch('https://api.pinterest.com/v5/media/' + media_id, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
await timer(3000);
|
||||
|
|
@ -261,4 +263,68 @@ export class PinterestProvider extends SocialAbstract implements SocialProvider
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
|
||||
const {
|
||||
all: { daily_metrics },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.pinterest.com/v5/user_account/analytics?start_date=${since}&end_date=${until}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return daily_metrics.reduce(
|
||||
(acc: any, item: any) => {
|
||||
if (typeof item.metrics.PIN_CLICK_RATE !== 'undefined') {
|
||||
acc[0].data.push({
|
||||
date: item.date,
|
||||
total: item.metrics.PIN_CLICK_RATE,
|
||||
});
|
||||
|
||||
acc[1].data.push({
|
||||
date: item.date,
|
||||
total: item.metrics.IMPRESSION,
|
||||
});
|
||||
|
||||
acc[2].data.push({
|
||||
date: item.date,
|
||||
total: item.metrics.PIN_CLICK,
|
||||
});
|
||||
|
||||
acc[3].data.push({
|
||||
date: item.date,
|
||||
total: item.metrics.ENGAGEMENT,
|
||||
});
|
||||
|
||||
acc[4].data.push({
|
||||
date: item.date,
|
||||
total: item.metrics.SAVE,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[
|
||||
{ label: 'Pin click rate', data: [] as any[] },
|
||||
{ label: 'Impressions', data: [] as any[] },
|
||||
{ label: 'Pin Clicks', data: [] as any[] },
|
||||
{ label: 'Engagement', data: [] as any[] },
|
||||
{ label: 'Saves', data: [] as any[] },
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,74 @@
|
|||
export interface IAuthenticator {
|
||||
authenticate(params: {code: string, codeVerifier: string, refresh?: string}): Promise<AuthTokenDetails>;
|
||||
refreshToken(refreshToken: string): Promise<AuthTokenDetails>;
|
||||
generateAuthUrl(refresh?: string): Promise<GenerateAuthUrlResponse>;
|
||||
authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}): Promise<AuthTokenDetails>;
|
||||
refreshToken(refreshToken: string): Promise<AuthTokenDetails>;
|
||||
generateAuthUrl(refresh?: string): Promise<GenerateAuthUrlResponse>;
|
||||
analytics?(id: string, accessToken: string, date: number): Promise<AnalyticsData[]>;
|
||||
}
|
||||
|
||||
export interface AnalyticsData {
|
||||
label: string;
|
||||
data: Array<{ total: string; date: string }>;
|
||||
percentageChange: number;
|
||||
}
|
||||
|
||||
export type GenerateAuthUrlResponse = {
|
||||
url: string,
|
||||
codeVerifier: string,
|
||||
state: string
|
||||
}
|
||||
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
|
||||
picture?: string;
|
||||
username: string;
|
||||
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
|
||||
picture?: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export interface ISocialMediaIntegration {
|
||||
post(id: string, accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]>; // Schedules a new post
|
||||
post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]>; // Schedules a new post
|
||||
}
|
||||
|
||||
export type PostResponse = {
|
||||
id: string; // The db internal id of the post
|
||||
postId: string; // The ID of the scheduled post returned by the platform
|
||||
releaseURL: string; // The URL of the post on the platform
|
||||
status: string; // Status of the operation or initial post status
|
||||
id: string; // The db internal id of the post
|
||||
postId: string; // The ID of the scheduled post returned by the platform
|
||||
releaseURL: string; // The URL of the post on the platform
|
||||
status: string; // Status of the operation or initial post status
|
||||
};
|
||||
|
||||
export type PostDetails<T = any> = {
|
||||
id: string;
|
||||
message: string;
|
||||
settings: T;
|
||||
media?: MediaContent[];
|
||||
poll?: PollDetails;
|
||||
id: string;
|
||||
message: string;
|
||||
settings: T;
|
||||
media?: MediaContent[];
|
||||
poll?: PollDetails;
|
||||
};
|
||||
|
||||
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
|
||||
path: string;
|
||||
options: string[]; // Array of poll options
|
||||
duration: number; // Duration in hours for which the poll will be active
|
||||
};
|
||||
|
||||
export interface SocialProvider extends IAuthenticator, ISocialMediaIntegration {
|
||||
identifier: string;
|
||||
name: string;
|
||||
isBetweenSteps: boolean;
|
||||
}
|
||||
export type MediaContent = {
|
||||
type: 'image' | 'video'; // Type of the media content
|
||||
url: string; // URL of the media file, if it's already hosted somewhere
|
||||
path: string;
|
||||
};
|
||||
|
||||
export interface SocialProvider
|
||||
extends IAuthenticator,
|
||||
ISocialMediaIntegration {
|
||||
identifier: string;
|
||||
name: string;
|
||||
isBetweenSteps: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,103 +73,18 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const getAccessToken = await (
|
||||
await this.fetch(
|
||||
'https://graph.facebook.com/v20.0/oauth/access_token' +
|
||||
`?client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/facebook${
|
||||
params.refresh ? `?refresh=${params.refresh}` : ''
|
||||
}`
|
||||
)}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
`&code=${params.code}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const { access_token } = await (
|
||||
await this.fetch(
|
||||
'https://graph.facebook.com/v20.0/oauth/access_token' +
|
||||
'?grant_type=fb_exchange_token' +
|
||||
`&client_id=${process.env.FACEBOOK_APP_ID}` +
|
||||
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
|
||||
`&fb_exchange_token=${getAccessToken.access_token}&fields=access_token,expires_in`
|
||||
)
|
||||
).json();
|
||||
|
||||
if (params.refresh) {
|
||||
const information = await this.fetchPageInformation(
|
||||
access_token,
|
||||
params.refresh
|
||||
);
|
||||
return {
|
||||
id: information.id,
|
||||
name: information.name,
|
||||
accessToken: information.access_token,
|
||||
refreshToken: information.access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: information.picture,
|
||||
username: information.username,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v19.0/me?fields=id,name,picture&access_token=${access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
console.log(params);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
id: '',
|
||||
name: '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
picture: url,
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async pages(accessToken: string) {
|
||||
const { data } = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async fetchPageInformation(accessToken: string, pageId: string) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
access_token,
|
||||
username,
|
||||
picture: {
|
||||
data: { url },
|
||||
},
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
access_token,
|
||||
picture: url,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
|
|
@ -8,9 +9,11 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client';
|
||||
import * as console from 'node:console';
|
||||
import axios from 'axios';
|
||||
import axios, { all } from 'axios';
|
||||
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import * as process from 'node:process';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const clientAndYoutube = () => {
|
||||
const client = new google.auth.OAuth2({
|
||||
|
|
@ -25,13 +28,19 @@ const clientAndYoutube = () => {
|
|||
auth: newClient,
|
||||
});
|
||||
|
||||
const youtubeAnalytics = (newClient: OAuth2Client) =>
|
||||
google.youtubeAnalytics({
|
||||
version: 'v2',
|
||||
auth: newClient,
|
||||
});
|
||||
|
||||
const oauth2 = (newClient: OAuth2Client) =>
|
||||
google.oauth2({
|
||||
version: 'v2',
|
||||
auth: newClient,
|
||||
});
|
||||
|
||||
return { client, youtube, oauth2 };
|
||||
return { client, youtube, oauth2, youtubeAnalytics };
|
||||
};
|
||||
|
||||
export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
||||
|
|
@ -79,6 +88,8 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
|||
'https://www.googleapis.com/auth/youtube.readonly',
|
||||
'https://www.googleapis.com/auth/youtube.upload',
|
||||
'https://www.googleapis.com/auth/youtubepartner',
|
||||
'https://www.googleapis.com/auth/youtubepartner',
|
||||
'https://www.googleapis.com/auth/yt-analytics.readonly',
|
||||
],
|
||||
}),
|
||||
codeVerifier: makeId(10),
|
||||
|
|
@ -162,4 +173,87 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const endDate = dayjs().format('YYYY-MM-DD');
|
||||
const startDate = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
|
||||
const { client, youtubeAnalytics } = clientAndYoutube();
|
||||
client.setCredentials({ access_token: accessToken });
|
||||
const youtubeClient = youtubeAnalytics(client);
|
||||
const { data } = await youtubeClient.reports.query({
|
||||
ids: 'channel==MINE',
|
||||
startDate,
|
||||
endDate,
|
||||
metrics:
|
||||
'views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage,subscribersGained,likes,subscribersLost',
|
||||
dimensions: 'day',
|
||||
sort: 'day',
|
||||
});
|
||||
|
||||
const columns = data?.columnHeaders?.map((p) => p.name)!;
|
||||
const mappedData = data?.rows?.map((p) => {
|
||||
return columns.reduce((acc, curr, index) => {
|
||||
acc[curr!] = p[index];
|
||||
return acc;
|
||||
}, {} as any);
|
||||
});
|
||||
|
||||
const acc = [] as any[];
|
||||
acc.push({
|
||||
label: 'Estimated Minutes Watched',
|
||||
data: mappedData?.map((p: any) => ({
|
||||
total: p.estimatedMinutesWatched,
|
||||
date: p.day,
|
||||
})),
|
||||
});
|
||||
|
||||
acc.push({
|
||||
label: 'Average View Duration',
|
||||
average: true,
|
||||
data: mappedData?.map((p: any) => ({
|
||||
total: p.averageViewDuration,
|
||||
date: p.day,
|
||||
})),
|
||||
});
|
||||
|
||||
acc.push({
|
||||
label: 'Average View Percentage',
|
||||
average: true,
|
||||
data: mappedData?.map((p: any) => ({
|
||||
total: p.averageViewPercentage,
|
||||
date: p.day,
|
||||
})),
|
||||
});
|
||||
|
||||
acc.push({
|
||||
label: 'Subscribers Gained',
|
||||
data: mappedData?.map((p: any) => ({
|
||||
total: p.subscribersGained,
|
||||
date: p.day,
|
||||
})),
|
||||
});
|
||||
|
||||
acc.push({
|
||||
label: 'Subscribers Lost',
|
||||
data: mappedData?.map((p: any) => ({
|
||||
total: p.subscribersLost,
|
||||
date: p.day,
|
||||
})),
|
||||
});
|
||||
|
||||
acc.push({
|
||||
label: 'Likes',
|
||||
data: mappedData?.map((p: any) => ({
|
||||
total: p.likes,
|
||||
date: p.day,
|
||||
})),
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,18 @@ export const Select: FC<
|
|||
disableForm?: boolean;
|
||||
label: string;
|
||||
name: string;
|
||||
hideErrors?: boolean;
|
||||
}
|
||||
> = (props) => {
|
||||
const { label, className, disableForm, error, extraForm, ...rest } = props;
|
||||
const {
|
||||
label,
|
||||
className,
|
||||
hideErrors,
|
||||
disableForm,
|
||||
error,
|
||||
extraForm,
|
||||
...rest
|
||||
} = props;
|
||||
const form = useFormContext();
|
||||
const err = useMemo(() => {
|
||||
if (error) return error;
|
||||
|
|
@ -27,7 +36,7 @@ export const Select: FC<
|
|||
}, [form?.formState?.errors?.[props?.name!]?.message, error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className={clsx("flex flex-col", label ? 'gap-[6px]' : '')}>
|
||||
<div className={`${interClass} text-[14px]`}>{label}</div>
|
||||
<select
|
||||
{...(disableForm ? {} : form.register(props.name, extraForm))}
|
||||
|
|
@ -37,7 +46,9 @@ export const Select: FC<
|
|||
)}
|
||||
{...rest}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
{!hideErrors && (
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
"bcrypt": "^5.1.1",
|
||||
"bufferutil": "^4.0.8",
|
||||
"bullmq": "^5.1.5",
|
||||
"cache-manager-redis-store": "^2.0.0",
|
||||
"chart.js": "^4.4.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
|
|
@ -119,6 +120,7 @@
|
|||
"@swc/cli": "~0.1.62",
|
||||
"@swc/core": "~1.3.85",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/cache-manager-redis-store": "^2.0.4",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/node": "18.16.9",
|
||||
|
|
@ -12907,6 +12909,22 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cache-manager": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.6.tgz",
|
||||
"integrity": "sha512-8qL93MF05/xrzFm/LSPtzNEOE1eQF3VwGHAcQEylgp5hDSTe41jtFwbSYAPfyYcVa28y1vYSjIt0c1fLLUiC/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/cache-manager-redis-store": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/cache-manager-redis-store/-/cache-manager-redis-store-2.0.4.tgz",
|
||||
"integrity": "sha512-EG4ac1KsUr07uv6N/O0X1OaQBNVKShVUxn+GwJQQpUkTEi4+KJl6yvqfwc4uTPT1+pwfKRgQhCoHQQCd/ObkZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/cache-manager": "*",
|
||||
"@types/redis": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
|
|
@ -13324,6 +13342,15 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/redis": {
|
||||
"version": "2.8.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
|
||||
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/remove-markdown": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/remove-markdown/-/remove-markdown-0.3.4.tgz",
|
||||
|
|
@ -15981,6 +16008,43 @@
|
|||
"promise-coalesce": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-store": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-2.0.0.tgz",
|
||||
"integrity": "sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==",
|
||||
"dependencies": {
|
||||
"redis": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-store/node_modules/denque": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
|
||||
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-store/node_modules/redis": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
||||
"dependencies": {
|
||||
"denque": "^1.5.0",
|
||||
"redis-commands": "^1.7.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-redis"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager/node_modules/lru-cache": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
|
||||
|
|
@ -33636,6 +33700,11 @@
|
|||
"@redis/time-series": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-commands": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
|
||||
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
"bcrypt": "^5.1.1",
|
||||
"bufferutil": "^4.0.8",
|
||||
"bullmq": "^5.1.5",
|
||||
"cache-manager-redis-store": "^2.0.0",
|
||||
"chart.js": "^4.4.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
|
|
@ -123,6 +124,7 @@
|
|||
"@swc/cli": "~0.1.62",
|
||||
"@swc/core": "~1.3.85",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/cache-manager-redis-store": "^2.0.4",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/node": "18.16.9",
|
||||
|
|
|
|||
Loading…
Reference in New Issue