From d7cc0d20a16cd6640d0f60f5afa2342d8dc2265e Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 5 Jun 2024 19:14:13 +0700 Subject: [PATCH] feat: insights --- .../providers/dribbble/dribbble.provider.tsx | 160 ++++++++++++++++ .../providers/dribbble/dribbble.teams.tsx | 47 +++++ .../launches/providers/show.all.providers.tsx | 2 + .../src/dtos/posts/create.post.dto.ts | 2 + .../posts/providers-settings/dribbble.dto.ts | 12 ++ .../integrations/social/dribbble.provider.ts | 172 ++++-------------- .../integrations/social/facebook.provider.ts | 2 +- 7 files changed, 261 insertions(+), 136 deletions(-) create mode 100644 apps/frontend/src/components/launches/providers/dribbble/dribbble.provider.tsx create mode 100644 apps/frontend/src/components/launches/providers/dribbble/dribbble.teams.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/dribbble.dto.ts diff --git a/apps/frontend/src/components/launches/providers/dribbble/dribbble.provider.tsx b/apps/frontend/src/components/launches/providers/dribbble/dribbble.provider.tsx new file mode 100644 index 00000000..42739668 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/dribbble/dribbble.provider.tsx @@ -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 ( +
+ + +
+ ); +}; +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 ( +
+
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
1m
+
+
+
+
+
+        {!!firstPost?.images?.length && (
+          
+ {firstPost.images.map((image, index) => ( + + + + ))} +
+ )} +
+ {morePosts.map((p, index) => ( +
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
+ {p.text} +
+ + {!!p?.images?.length && ( +
+ {p.images.map((image, index) => ( + +
+ +
+
+ ))} +
+ )} +
+
+ ))} +
+ ); +}; + +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.'; + } +); diff --git a/apps/frontend/src/components/launches/providers/dribbble/dribbble.teams.tsx b/apps/frontend/src/components/launches/providers/dribbble/dribbble.teams.tsx new file mode 100644 index 00000000..22d79be2 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/dribbble/dribbble.teams.tsx @@ -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(); + const { getValues } = useSettings(); + const [currentMedia, setCurrentMedia] = useState(); + + 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 ( + + ); +}; diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index 2ee9aa87..cfb7997e 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -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}, ]; 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 c314c473..563b4c65 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts @@ -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' }, ], }, }) diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dribbble.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dribbble.dto.ts new file mode 100644 index 00000000..882429a4 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dribbble.dto.ts @@ -0,0 +1,12 @@ +import { IsDefined, IsOptional, IsString, IsUrl } from 'class-validator'; + +export class DribbbleDto { + @IsString() + @IsDefined() + title: string; + + @IsString() + @IsOptional() + @IsUrl() + team: string; +} diff --git a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts index 0d76c8b2..f95fafeb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts @@ -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[] + postDetails: PostDetails[] ): Promise { - 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( diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index 9ba02834..ad9bcb2b 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -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, };