);
});
+
+const Duplicate = () => {
+ return (
+
+ );
+};
+
+const Preview = () => {
+ return (
+
+ );
+};
+
+export const Statistics = () => {
+ return (
+
+ );
+};
diff --git a/apps/frontend/src/components/launches/statistics.tsx b/apps/frontend/src/components/launches/statistics.tsx
new file mode 100644
index 00000000..90e050d1
--- /dev/null
+++ b/apps/frontend/src/components/launches/statistics.tsx
@@ -0,0 +1,73 @@
+import React, { FC, Fragment, useCallback } from 'react';
+import { useModals } from '@mantine/modals';
+import useSWR from 'swr';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+
+export const StatisticsModal: FC<{ postId: string }> = (props) => {
+ const { postId } = props;
+ const modals = useModals();
+ const fetch = useFetch();
+
+ const loadStatistics = useCallback(async () => {
+ return (await fetch(`/posts/${postId}/statistics`)).json();
+ }, [postId]);
+
+ const closeAll = useCallback(() => {
+ modals.closeAll();
+ }, []);
+
+ const { data, isLoading } = useSWR(
+ `/posts/${postId}/statistics`,
+ loadStatistics
+ );
+
+ return (
+
+
+
Statistics
+ {isLoading ? (
+
Loading
+ ) : (
+ <>
+ {data.clicks.length === 0 ? (
+ 'No Results'
+ ) : (
+ <>
+
+
Short Link
+
Original Link
+
Clicks
+ {data.clicks.map((p: any) => (
+
+ {p.short}
+ {p.original}
+ {p.clicks}
+
+ ))}
+
+ >
+ )}
+ >
+ )}
+
+ );
+};
diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
index 7b565203..9345983e 100644
--- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
@@ -28,6 +28,7 @@ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
+import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
@Global()
@Module({
@@ -64,6 +65,7 @@ import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
OpenaiService,
EmailService,
TrackService,
+ ShortLinkService,
],
get exports() {
return this.providers;
diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
index d4320b6e..30c58802 100644
--- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
@@ -21,6 +21,7 @@ import { timer } from '@gitroom/helpers/utils/timer';
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import utc from 'dayjs/plugin/utc';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
+import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
dayjs.extend(utc);
type PostWithConditionals = Post & {
@@ -38,9 +39,20 @@ export class PostsService {
private _messagesService: MessagesService,
private _stripeService: StripeService,
private _integrationService: IntegrationService,
- private _mediaService: MediaService
+ private _mediaService: MediaService,
+ private _shortLinkService: ShortLinkService
) {}
+ async getStatistics(orgId: string, id: string) {
+ const getPost = await this.getPostsRecursively(id, true, orgId, true);
+ const content = getPost.map((p) => p.content);
+ const shortLinksTracking = await this._shortLinkService.getStatistics(content);
+
+ return {
+ clicks: shortLinksTracking
+ }
+ }
+
async getPostsRecursively(
id: string,
includeIntegration = false,
@@ -554,6 +566,14 @@ export class PostsService {
async createPost(orgId: string, body: CreatePostDto) {
const postList = [];
for (const post of body.posts) {
+ const messages = post.value.map(p => p.content);
+ const updateContent = !body.shortLink ? messages : await this._shortLinkService.convertTextToShortLinks(orgId, messages);
+
+ post.value = post.value.map((p, i) => ({
+ ...p,
+ content: updateContent[i],
+ }));
+
const { previousPost, posts } =
await this._postRepository.createOrUpdatePost(
body.type,
@@ -757,6 +777,7 @@ export class PostsService {
type: 'draft',
date: randomDate,
order: '',
+ shortLink: false,
posts: [
{
group,
diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts
index 20bcd248..3965f710 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts
@@ -1,5 +1,5 @@
import {
- ArrayMinSize, IsArray, IsDateString, IsDefined, IsIn, IsOptional, IsString, MinLength, ValidateIf, ValidateNested
+ ArrayMinSize, IsArray, IsBoolean, IsDateString, IsDefined, IsIn, IsOptional, IsString, MinLength, ValidateIf, ValidateNested
} from 'class-validator';
import { Type } from 'class-transformer';
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
@@ -89,6 +89,10 @@ export class CreatePostDto {
@IsString()
order: string;
+ @IsDefined()
+ @IsBoolean()
+ shortLink: boolean;
+
@IsDefined()
@IsDateString()
date: string;
diff --git a/libraries/nestjs-libraries/src/short-linking/providers/dub.ts b/libraries/nestjs-libraries/src/short-linking/providers/dub.ts
new file mode 100644
index 00000000..cc61a150
--- /dev/null
+++ b/libraries/nestjs-libraries/src/short-linking/providers/dub.ts
@@ -0,0 +1,80 @@
+import { ShortLinking } from '@gitroom/nestjs-libraries/short-linking/short-linking.interface';
+
+const options = {
+ headers: {
+ Authorization: `Bearer ${process.env.DUB_TOKEN}`,
+ 'Content-Type': 'application/json',
+ },
+};
+
+export class Dub implements ShortLinking {
+ shortLinkDomain = 'dub.sh';
+
+ async linksStatistics(links: string[]) {
+ return Promise.all(
+ links.map(async (link) => {
+ const response = await (
+ await fetch(`https://api.dub.co/links/info?domain=${this.shortLinkDomain}&key=${link.split('/').pop()}`, options)
+ ).json();
+
+ return {
+ short: link,
+ original: response.url,
+ clicks: response.clicks,
+ };
+ })
+ );
+ }
+
+ async convertLinkToShortLink(id: string, link: string) {
+ return (
+ await (
+ await fetch(`https://api.dub.co/links`, {
+ ...options,
+ method: 'POST',
+ body: JSON.stringify({
+ url: link,
+ tenantId: id,
+ domain: this.shortLinkDomain,
+ }),
+ })
+ ).json()
+ ).shortLink;
+ }
+
+ async convertShortLinkToLink(shortLink: string) {
+ return await (
+ await (
+ await fetch(
+ `https://api.dub.co/links/info?domain=${shortLink}`,
+ options
+ )
+ ).json()
+ ).url;
+ }
+
+ // recursive functions that gets maximum 100 links per request if there are less than 100 links stop the recursion
+ async getAllLinksStatistics(
+ id: string,
+ page = 1
+ ): Promise<{ short: string; original: string; clicks: string }[]> {
+ const response = await (
+ await fetch(
+ `https://api.dub.co/links?tenantId=${id}&page=${page}&pageSize=100`,
+ options
+ )
+ ).json();
+
+ const mapLinks = response.links.map((link: any) => ({
+ short: link,
+ original: response.url,
+ clicks: response.clicks,
+ }));
+
+ if (mapLinks.length < 100) {
+ return mapLinks;
+ }
+
+ return [...mapLinks, ...(await this.getAllLinksStatistics(id, page + 1))];
+ }
+}
diff --git a/libraries/nestjs-libraries/src/short-linking/providers/empty.ts b/libraries/nestjs-libraries/src/short-linking/providers/empty.ts
new file mode 100644
index 00000000..53d6bf5a
--- /dev/null
+++ b/libraries/nestjs-libraries/src/short-linking/providers/empty.ts
@@ -0,0 +1,21 @@
+import { ShortLinking } from '@gitroom/nestjs-libraries/short-linking/short-linking.interface';
+
+export class Empty implements ShortLinking {
+ shortLinkDomain = 'empty';
+
+ async linksStatistics(links: string[]) {
+ return [];
+ }
+
+ async convertLinkToShortLink(link: string) {
+ return '';
+ }
+
+ async convertShortLinkToLink(shortLink: string) {
+ return '';
+ }
+
+ getAllLinksStatistics(id: string, page: number): Promise<{ short: string; original: string; clicks: string }[]> {
+ return Promise.resolve([]);
+ }
+}
diff --git a/libraries/nestjs-libraries/src/short-linking/short-linking.interface.ts b/libraries/nestjs-libraries/src/short-linking/short-linking.interface.ts
new file mode 100644
index 00000000..74e2360a
--- /dev/null
+++ b/libraries/nestjs-libraries/src/short-linking/short-linking.interface.ts
@@ -0,0 +1,7 @@
+export interface ShortLinking {
+ shortLinkDomain: string;
+ linksStatistics(links: string[]): Promise<{short: string; original: string, clicks: string}[]>;
+ convertLinkToShortLink(id: string, link: string): Promise
;
+ convertShortLinkToLink(shortLink: string): Promise;
+ getAllLinksStatistics(id: string, page: number): Promise<{short: string; original: string, clicks: string}[]>;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/short-linking/short.link.service.ts b/libraries/nestjs-libraries/src/short-linking/short.link.service.ts
new file mode 100644
index 00000000..cdff93ce
--- /dev/null
+++ b/libraries/nestjs-libraries/src/short-linking/short.link.service.ts
@@ -0,0 +1,131 @@
+import { Dub } from '@gitroom/nestjs-libraries/short-linking/providers/dub';
+import { Empty } from '@gitroom/nestjs-libraries/short-linking/providers/empty';
+import { ShortLinking } from '@gitroom/nestjs-libraries/short-linking/short-linking.interface';
+import { Injectable } from '@nestjs/common';
+
+const getProvider = (): ShortLinking => {
+ if (process.env.DUB_TOKEN) {
+ return new Dub();
+ }
+
+ return new Empty();
+};
+
+@Injectable()
+export class ShortLinkService {
+ static provider = getProvider();
+
+ askShortLinkedin(messages: string[]): boolean {
+ if (ShortLinkService.provider.shortLinkDomain === 'empty') {
+ return false;
+ }
+
+ const mergeMessages = messages.join(' ');
+ const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/g;
+ const urls = mergeMessages.match(urlRegex);
+ if (!urls) {
+ // No URLs found, return the original text
+ return false;
+ }
+
+ return urls.some((url) => url.indexOf(ShortLinkService.provider.shortLinkDomain) === -1);
+ }
+
+ async convertTextToShortLinks(id: string, messages: string[]) {
+ if (ShortLinkService.provider.shortLinkDomain === 'empty') {
+ return messages;
+ }
+
+ const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/g;
+ return Promise.all(
+ messages.map(async (text) => {
+ const urls = text.match(urlRegex);
+ if (!urls) {
+ // No URLs found, return the original text
+ return text;
+ }
+
+ const replacementMap: Record = {};
+
+ // Process each URL asynchronously
+ await Promise.all(
+ urls.map(async (url) => {
+ if (url.indexOf(ShortLinkService.provider.shortLinkDomain) === -1) {
+ replacementMap[url] =
+ await ShortLinkService.provider.convertLinkToShortLink(id, url);
+ } else {
+ replacementMap[url] = url; // Keep the original URL if it matches the prefix
+ }
+ })
+ );
+
+ // Replace the URLs in the text with their replacements
+ return text.replace(urlRegex, (url) => replacementMap[url]);
+ })
+ );
+ }
+
+ async convertShortLinksToLinks(messages: string[]) {
+ if (ShortLinkService.provider.shortLinkDomain === 'empty') {
+ return messages;
+ }
+
+ const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/g;
+ return Promise.all(
+ messages.map(async (text) => {
+ const urls = text.match(urlRegex);
+ if (!urls) {
+ // No URLs found, return the original text
+ return text;
+ }
+
+ const replacementMap: Record = {};
+
+ // Process each URL asynchronously
+ await Promise.all(
+ urls.map(async (url) => {
+ if (url.indexOf(ShortLinkService.provider.shortLinkDomain) > -1) {
+ replacementMap[url] =
+ await ShortLinkService.provider.convertShortLinkToLink(url);
+ } else {
+ replacementMap[url] = url; // Keep the original URL if it matches the prefix
+ }
+ })
+ );
+
+ // Replace the URLs in the text with their replacements
+ return text.replace(urlRegex, (url) => replacementMap[url]);
+ })
+ );
+ }
+
+ async getStatistics(messages: string[]) {
+ if (ShortLinkService.provider.shortLinkDomain === 'empty') {
+ return [];
+ }
+
+ const mergeMessages = messages.join(' ');
+ const regex = new RegExp(
+ `https?://${ShortLinkService.provider.shortLinkDomain.replace(
+ '.',
+ '\\.'
+ )}/[^\\s]*`,
+ 'g'
+ );
+ const urls = mergeMessages.match(regex);
+ if (!urls) {
+ // No URLs found, return the original text
+ return [];
+ }
+
+ return ShortLinkService.provider.linksStatistics(urls);
+ }
+
+ async getAllLinks(id: string) {
+ if (ShortLinkService.provider.shortLinkDomain === 'empty') {
+ return [];
+ }
+
+ return ShortLinkService.provider.getAllLinksStatistics(id, 1);
+ }
+}