feat: insights
This commit is contained in:
parent
7777d3a03b
commit
d7cc0d20a1
|
|
@ -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.';
|
||||
}
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { IsDefined, IsOptional, IsString, IsUrl } from 'class-validator';
|
||||
|
||||
export class DribbbleDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
team: string;
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue