feat: insights

This commit is contained in:
Nevo David 2024-06-05 19:14:13 +07:00
parent 7777d3a03b
commit d7cc0d20a1
7 changed files with 261 additions and 136 deletions

View File

@ -0,0 +1,160 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import {
afterLinkedinCompanyPreventRemove,
linkedinCompanyPreventRemove,
} from '@gitroom/helpers/utils/linkedin.company.prevent.remove';
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { DribbbleTeams } from '@gitroom/frontend/components/launches/providers/dribbble/dribbble.teams';
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
const DribbbleSettings: FC = () => {
const { register, control } = useSettings();
return (
<div className="flex flex-col">
<Input label={'Title'} {...register('title')} />
<DribbbleTeams {...register('team')} />
</div>
);
};
const DribbblePreview: FC = (props) => {
const { value: topValue, integration } = useIntegration();
const mediaDir = useMediaDirectory();
const newValues = useFormatting(topValue, {
removeMarkdown: true,
saveBreaklines: true,
beforeSpecialFunc: (text: string) => {
return linkedinCompanyPreventRemove(text);
},
specialFunc: (text: string) => {
return afterLinkedinCompanyPreventRemove(text.slice(0, 280));
},
});
const [firstPost, ...morePosts] = newValues;
if (!firstPost) {
return null;
}
return (
<div className="rounded-[8px] flex flex-col gap-[8px] border border-black/90 w-[555px] pt-[12px] pl-[16px] pb-[12px] pr-[40px] bg-white text-black font-['helvetica']">
<div className="flex gap-[8px]">
<div className="w-[48px] h-[48px]">
<img
src={integration?.picture}
alt="x"
className="rounded-full w-full h-full relative z-[2]"
/>
</div>
<div className="flex flex-col leading-[16px]">
<div className="text-[14px] font-[600]">{integration?.name}</div>
<div className="text-[12px] font-[400] text-black/60">
CEO @ Gitroom
</div>
<div className="text-[12px] font-[400] text-black/60">1m</div>
</div>
</div>
<div>
<pre
className="font-['helvetica'] text-[14px] font-[400] text-wrap"
dangerouslySetInnerHTML={{ __html: firstPost?.text }}
/>
{!!firstPost?.images?.length && (
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
{firstPost.images.map((image, index) => (
<a
key={`image_${index}`}
href={mediaDir.set(image.path)}
className="flex-1"
target="_blank"
>
<VideoOrImage autoplay={true} src={mediaDir.set(image.path)} />
</a>
))}
</div>
)}
</div>
{morePosts.map((p, index) => (
<div className="flex gap-[8px]" key={index}>
<div className="w-[40px] h-[40px]">
<img
src={integration?.picture}
alt="x"
className="rounded-full w-full h-full relative z-[2]"
/>
</div>
<div className="flex-1 flex flex-col leading-[16px] bg-[#F2F2F2] w-full pt-[8px] pr-[64px] pl-[12px] pb-[8px] rounded-[8px]">
<div className="text-[14px] font-[600]">{integration?.name}</div>
<div className="text-[12px] font-[400] text-black/60">
CEO @ Gitroom
</div>
<div className="text-[14px] mt-[8px] font-[400] text-black/90">
{p.text}
</div>
{!!p?.images?.length && (
<div className="w-full h-[120px] flex overflow-hidden mt-[12px] gap-[3px]">
{p.images.map((image, index) => (
<a
key={`image_${index}`}
href={mediaDir.set(image.path)}
target="_blank"
>
<div className="w-[120px] h-full">
<VideoOrImage
autoplay={true}
src={mediaDir.set(image.path)}
/>
</div>
</a>
))}
</div>
)}
</div>
</div>
))}
</div>
);
};
export default withProvider(
DribbbleSettings,
DribbblePreview,
DribbbleDto,
async ([firstItem, ...otherItems]) => {
const isMp4 = firstItem?.find((item) => item.path.indexOf('mp4') > -1);
if (firstItem.length !== 1) {
return 'Dribbble requires one item';
}
if (isMp4) {
return 'Dribbble does not support mp4 files';
}
const details = await new Promise<{width: number, height: number}>((resolve, reject) => {
const url = new Image();
url.onload = function() {
// @ts-ignore
resolve({width: this.width, height: this.height});
}
url.src = firstItem[0].path;
});
if (
(details?.width === 400 && details?.height === 300) ||
(details?.width === 800 && details?.height === 600)
) {
return true;
}
return 'Invalid image size. Dribbble requires 400x300 or 800x600 px images.';
}
);

View File

@ -0,0 +1,47 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const DribbbleTeams: FC<{
name: string;
onChange: (event: { target: { value: string; name: string } }) => void;
}> = (props) => {
const { onChange, name } = props;
const customFunc = useCustomProviderFunction();
const [orgs, setOrgs] = useState<undefined|any[]>();
const { getValues } = useSettings();
const [currentMedia, setCurrentMedia] = useState<string|undefined>();
const onChangeInner = (event: { target: { value: string, name: string } }) => {
setCurrentMedia(event.target.value);
onChange(event);
};
useEffect(() => {
customFunc.get('teams').then((data) => setOrgs(data));
const settings = getValues()[props.name];
if (settings) {
setCurrentMedia(settings);
}
}, []);
if (!orgs) {
return null;
}
if (!orgs.length) {
return <></>;
}
return (
<Select name={name} label="Select a team" onChange={onChangeInner} value={currentMedia}>
<option value="">--Select--</option>
{orgs.map((org: any) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</Select>
);
};

View File

@ -11,6 +11,7 @@ import InstagramProvider from '@gitroom/frontend/components/launches/providers/i
import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.provider';
import DribbbleProvider from '@gitroom/frontend/components/launches/providers/dribbble/dribbble.provider';
export const Providers = [
{identifier: 'devto', component: DevtoProvider},
@ -25,6 +26,7 @@ export const Providers = [
{identifier: 'youtube', component: YoutubeProvider},
{identifier: 'tiktok', component: TiktokProvider},
{identifier: 'pinterest', component: PinterestProvider},
{identifier: 'dribbble', component: DribbbleProvider},
];

View File

@ -10,6 +10,7 @@ import {HashnodeSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/provider
import {RedditSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto";
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/pinterest.dto';
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
export class EmptySettings {}
export class Integration {
@ -64,6 +65,7 @@ export class Post {
{ value: RedditSettingsDto, name: 'reddit' },
{ value: YoutubeSettingsDto, name: 'youtube' },
{ value: PinterestSettingsDto, name: 'pinterest' },
{ value: DribbbleDto, name: 'dribbble' },
],
},
})

View File

@ -0,0 +1,12 @@
import { IsDefined, IsOptional, IsString, IsUrl } from 'class-validator';
export class DribbbleDto {
@IsString()
@IsDefined()
title: string;
@IsString()
@IsOptional()
@IsUrl()
team: string;
}

View File

@ -12,6 +12,8 @@ import FormData from 'form-data';
import { timer } from '@gitroom/helpers/utils/timer';
import dayjs from 'dayjs';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
import mime from 'mime-types';
export class DribbbleProvider extends SocialAbstract implements SocialProvider {
identifier = 'dribbble';
@ -125,151 +127,51 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
};
}
async boards(accessToken: string) {
const { items } = await (
await this.fetch('https://api-sandbox.pinterest.com/v5/boards', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
).json();
return (
items?.map((item: any) => ({
name: item.name,
id: item.id,
})) || []
);
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails<PinterestSettingsDto>[]
postDetails: PostDetails<DribbbleDto>[]
): Promise<PostResponse[]> {
let mediaId = '';
const findMp4 = postDetails?.[0]?.media?.find(
(p) => (p.path?.indexOf('mp4') || -1) > -1
);
const picture = postDetails?.[0]?.media?.find(
(p) => (p.path?.indexOf('mp4') || -1) === -1
);
if (findMp4) {
const { upload_url, media_id, upload_parameters } = await (
await this.fetch('https://api-sandbox.pinterest.com/v5/media', {
method: 'POST',
body: JSON.stringify({
media_type: 'video',
}),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
).json();
const { data, status } = await axios.get(
postDetails?.[0]?.media?.[0]?.url!,
{
responseType: 'stream',
}
);
const formData = Object.keys(upload_parameters)
.filter((f) => f)
.reduce((acc, key) => {
acc.append(key, upload_parameters[key]);
return acc;
}, new FormData());
formData.append('file', data);
await axios.post(upload_url, formData);
let statusCode = '';
while (statusCode !== 'succeeded') {
console.log('trying');
const mediafile = await (
await this.fetch(
'https://api-sandbox.pinterest.com/v5/media/' + media_id,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
).json();
await timer(3000);
statusCode = mediafile.status;
const { data, status } = await axios.get(
postDetails?.[0]?.media?.[0]?.url!,
{
responseType: 'stream',
}
);
mediaId = media_id;
}
const slash = postDetails?.[0]?.media?.[0]?.url.split('/').at(-1);
const mapImages = postDetails?.[0]?.media?.map((m) => ({
url: m.url,
}));
const formData = new FormData();
formData.append('image', data, {
filename: slash,
contentType: mime.lookup(slash!) || '',
});
try {
const {
id: pId,
link,
...all
} = await (
await this.fetch('https://api-sandbox.pinterest.com/v5/pins', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(postDetails?.[0]?.settings.link
? { link: postDetails?.[0]?.settings.link }
: {}),
...(postDetails?.[0]?.settings.title
? { title: postDetails?.[0]?.settings.title }
: {}),
...(postDetails?.[0]?.settings.description
? { title: postDetails?.[0]?.settings.description }
: {}),
...(postDetails?.[0]?.settings.dominant_color
? { title: postDetails?.[0]?.settings.dominant_color }
: {}),
board_id: postDetails?.[0]?.settings.board,
media_source: mediaId
? {
source_type: 'video_id',
media_id: mediaId,
cover_image_url: picture?.url,
}
: mapImages?.length === 1
? {
source_type: 'image_url',
url: mapImages?.[0]?.url,
}
: {
source_type: 'multiple_image_urls',
items: mapImages,
},
}),
})
).json();
formData.append('title', postDetails[0].settings.title);
formData.append('description', postDetails[0].message);
return [
{
id: postDetails?.[0]?.id,
postId: pId,
releaseURL: `https://www.pinterest.com/pin/${pId}`,
status: 'success',
const data2 = await axios.post(
'https://api.dribbble.com/v2/shots',
formData,
{
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${accessToken}`,
},
];
} catch (err) {
console.log(err);
return [];
}
}
);
const location = data2.headers['location'];
const newId = location.split('/').at(-1);
return [
{
id: postDetails?.[0]?.id,
status: 'completed',
postId: newId,
releaseURL: `https://dribbble.com/shots/${newId}`,
},
];
}
analytics(

View File

@ -38,7 +38,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
}`
)}` +
`&state=${state}` +
'&scope=pages_show_list,business_management,pages_manage_posts,pages_manage_engagement,pages_read_engagement',
'&scope=pages_show_list,business_management,pages_manage_posts,pages_manage_engagement,pages_read_engagement,read_insights',
codeVerifier: makeId(10),
state,
};