feat: tag companies on linkedin

This commit is contained in:
Nevo David 2024-03-16 14:41:32 +07:00
parent a99b6c09df
commit b1efa7d97b
7 changed files with 286 additions and 18 deletions

View File

@ -39,6 +39,7 @@ import { postSelector } from '@gitroom/frontend/components/post-url-selector/pos
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { DatePicker } from '@gitroom/frontend/components/launches/helpers/date.picker';
import { arrayMoveImmutable } from 'array-move';
import { linkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
export const AddEditModal: FC<{
date: dayjs.Dayjs;

View File

@ -0,0 +1,195 @@
'use client';
import { EventEmitter } from 'events';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import {
executeCommand,
ExecuteState,
ICommand,
selectWord,
TextAreaTextApi,
} from '@uiw/react-md-editor';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { Input } from '@gitroom/react/form/input';
import { Button } from '@gitroom/react/form/button';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import dayjs from 'dayjs';
const postUrlEmitter = new EventEmitter();
export const ShowLinkedinCompany = () => {
const [showPostSelector, setShowPostSelector] = useState(false);
const [id, setId] = useState('');
const [callback, setCallback] = useState<{
callback: (tag: string) => void;
// eslint-disable-next-line @typescript-eslint/no-empty-function
} | null>({ callback: (tag: string) => {} } as any);
useEffect(() => {
postUrlEmitter.on(
'show',
(params: { id: string; callback: (url: string) => void }) => {
setCallback(params);
setId(params.id);
setShowPostSelector(true);
}
);
return () => {
setShowPostSelector(false);
setCallback(null);
setId('');
postUrlEmitter.removeAllListeners();
};
}, []);
const close = useCallback(() => {
setShowPostSelector(false);
setCallback(null);
setId('');
}, []);
if (!showPostSelector) {
return <></>;
}
return (
<LinkedinCompany id={id} onClose={close} onSelect={callback?.callback!} />
);
};
export const showPostSelector = (id: string) => {
return new Promise<string>((resolve) => {
postUrlEmitter.emit('show', {
id,
callback: (tag: string) => {
resolve(tag);
},
});
});
};
export const LinkedinCompany: FC<{
onClose: () => void;
onSelect: (tag: string) => void;
id: string;
}> = (props) => {
const { onClose, onSelect, id } = props;
const fetch = useFetch();
const [company, setCompany] = useState<any>(null);
const getCompany = async () => {
const {options} = await (
await fetch('/integrations/function', {
method: 'POST',
body: JSON.stringify({
id,
name: 'company',
data: {
url: company,
},
}),
})
).json();
onSelect(options.value);
onClose();
};
return (
<div className="text-white fixed left-0 top-0 bg-black/80 z-[300] w-full h-full p-[60px] animate-fade justify-center flex">
<div className="flex flex-col w-[500px] h-[250px] bg-[#0B101B] border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
<div className="flex">
<div className="flex-1">
<TopTitle title={'Select Company'} />
</div>
<button
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
type="button"
>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
<div className="mt-[10px]">
<Input
name="url"
disableForm={true}
label="URL"
value={company}
onChange={(e) => setCompany(e.target.value)}
placeholder="https://www.linkedin.com/company/gitroom"
/>
<Button onClick={getCompany}>Add</Button>
</div>
</div>
</div>
);
};
export const linkedinCompany = (identifier: string, id: string): ICommand[] => {
if (identifier !== 'linkedin') {
return [];
}
return [
{
name: 'linkedinCompany',
keyCommand: 'linkedinCompany',
shortcuts: 'ctrlcmd+p',
prefix: '',
suffix: '',
buttonProps: {
'aria-label': 'Add Post Url',
title: 'Add Post Url',
},
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 32 32"
fill="currentColor"
>
<path
d="M27 3H5C4.46957 3 3.96086 3.21071 3.58579 3.58579C3.21071 3.96086 3 4.46957 3 5V27C3 27.5304 3.21071 28.0391 3.58579 28.4142C3.96086 28.7893 4.46957 29 5 29H27C27.5304 29 28.0391 28.7893 28.4142 28.4142C28.7893 28.0391 29 27.5304 29 27V5C29 4.46957 28.7893 3.96086 28.4142 3.58579C28.0391 3.21071 27.5304 3 27 3ZM27 27H5V5H27V27ZM12 14V22C12 22.2652 11.8946 22.5196 11.7071 22.7071C11.5196 22.8946 11.2652 23 11 23C10.7348 23 10.4804 22.8946 10.2929 22.7071C10.1054 22.5196 10 22.2652 10 22V14C10 13.7348 10.1054 13.4804 10.2929 13.2929C10.4804 13.1054 10.7348 13 11 13C11.2652 13 11.5196 13.1054 11.7071 13.2929C11.8946 13.4804 12 13.7348 12 14ZM23 17.5V22C23 22.2652 22.8946 22.5196 22.7071 22.7071C22.5196 22.8946 22.2652 23 22 23C21.7348 23 21.4804 22.8946 21.2929 22.7071C21.1054 22.5196 21 22.2652 21 22V17.5C21 16.837 20.7366 16.2011 20.2678 15.7322C19.7989 15.2634 19.163 15 18.5 15C17.837 15 17.2011 15.2634 16.7322 15.7322C16.2634 16.2011 16 16.837 16 17.5V22C16 22.2652 15.8946 22.5196 15.7071 22.7071C15.5196 22.8946 15.2652 23 15 23C14.7348 23 14.4804 22.8946 14.2929 22.7071C14.1054 22.5196 14 22.2652 14 22V14C14.0012 13.7551 14.0923 13.5191 14.256 13.3369C14.4197 13.1546 14.6446 13.0388 14.888 13.0114C15.1314 12.9839 15.3764 13.0468 15.5765 13.188C15.7767 13.3292 15.918 13.539 15.9738 13.7775C16.6502 13.3186 17.4389 13.0526 18.2552 13.0081C19.0714 12.9637 19.8844 13.1424 20.6067 13.5251C21.329 13.9078 21.9335 14.48 22.3551 15.1803C22.7768 15.8806 22.9997 16.6825 23 17.5ZM12.5 10.5C12.5 10.7967 12.412 11.0867 12.2472 11.3334C12.0824 11.58 11.8481 11.7723 11.574 11.8858C11.2999 11.9994 10.9983 12.0291 10.7074 11.9712C10.4164 11.9133 10.1491 11.7704 9.93934 11.5607C9.72956 11.3509 9.5867 11.0836 9.52882 10.7926C9.47094 10.5017 9.50065 10.2001 9.61418 9.92597C9.72771 9.65189 9.91997 9.41762 10.1666 9.2528C10.4133 9.08797 10.7033 9 11 9C11.3978 9 11.7794 9.15804 12.0607 9.43934C12.342 9.72064 12.5 10.1022 12.5 10.5Z"
fill="currentColor"
/>
</svg>
),
execute: async (state: ExecuteState, api: TextAreaTextApi) => {
const newSelectionRange = selectWord({
text: state.text,
selection: state.selection,
prefix: state.command.prefix!,
suffix: state.command.suffix,
});
const state1 = api.setSelectionRange(newSelectionRange);
const media = await showPostSelector(id);
console.log(media);
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: media,
suffix: '',
});
},
},
];
};

View File

@ -28,6 +28,7 @@ import { newImage } from '@gitroom/frontend/components/launches/helpers/new.imag
import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { arrayMoveImmutable } from 'array-move';
import { linkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
// Simple component to change back to settings on after changing tab
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
@ -210,7 +211,11 @@ export const withProvider = (
setInPlaceValue(
editInPlace
? [{ content: '' }]
: props.value.map((p) => ({ id: p.id, content: p.content, image: p.image}))
: props.value.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))
);
}, [props.value, editInPlace]);
@ -280,6 +285,7 @@ export const withProvider = (
.filter((f) => f.name !== 'image'),
newImage,
postSelector(date),
...linkedinCompany(integration?.identifier!, integration?.id!),
]}
preview="edit"
// @ts-ignore

View File

@ -2,7 +2,6 @@
import { ReactNode, useCallback } from 'react';
import { Title } from '@gitroom/frontend/components/layout/title';
import { headers } from 'next/headers';
import { ContextWrapper } from '@gitroom/frontend/components/layout/user.context';
import { TopMenu } from '@gitroom/frontend/components/layout/top.menu';
import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper';
@ -16,11 +15,12 @@ import NotificationComponent from '@gitroom/frontend/components/notifications/no
import Link from 'next/link';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import weekOfYear from "dayjs/plugin/weekOfYear";
import isoWeek from "dayjs/plugin/isoWeek";
import isBetween from "dayjs/plugin/isBetween";
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import isoWeek from 'dayjs/plugin/isoWeek';
import isBetween from 'dayjs/plugin/isBetween';
import { ShowLinkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
dayjs.extend(utc);
dayjs.extend(weekOfYear);
@ -46,6 +46,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
<MantineWrapper>
<ToolTip />
<ShowMediaBoxModal />
<ShowLinkedinCompany />
<Toaster />
<ShowPostSelector />
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">

View File

@ -189,11 +189,11 @@ export const postSelector = (date: dayjs.Dayjs): ICommand => ({
width="13"
height="13"
viewBox="0 0 32 32"
fill="none"
fill="currentColor"
>
<path
d="M27.4602 14.6576C27.4039 14.4173 27.2893 14.1947 27.1264 14.0093C26.9635 13.824 26.7575 13.6817 26.5264 13.5951L19.7214 11.0438L21.4714 2.2938C21.5354 1.97378 21.4933 1.64163 21.3514 1.34773C21.2095 1.05382 20.9757 0.814201 20.6854 0.665207C20.395 0.516214 20.064 0.465978 19.7425 0.52212C19.421 0.578262 19.1266 0.737718 18.9039 0.976302L4.90393 15.9763C4.73549 16.1566 4.61413 16.3756 4.55059 16.614C4.48705 16.8525 4.4833 17.1028 4.53968 17.343C4.59605 17.5832 4.7108 17.8058 4.87377 17.9911C5.03673 18.1763 5.24287 18.3185 5.47393 18.4051L12.2789 20.9563L10.5289 29.7063C10.465 30.0263 10.5071 30.3585 10.649 30.6524C10.7908 30.9463 11.0247 31.1859 11.315 31.3349C11.6054 31.4839 11.9364 31.5341 12.2579 31.478C12.5794 31.4218 12.8738 31.2624 13.0964 31.0238L27.0964 16.0238C27.2647 15.8435 27.3859 15.6245 27.4494 15.3862C27.5128 15.1479 27.5165 14.8976 27.4602 14.6576ZM14.5064 25.1163L15.4714 20.2938C15.5412 19.9446 15.4845 19.5819 15.3113 19.2706C15.1382 18.9594 14.86 18.7199 14.5264 18.5951L8.62518 16.3838L17.4914 6.8838L16.5264 11.7063C16.4566 12.0555 16.5134 12.4182 16.6865 12.7295C16.8597 13.0407 17.1379 13.2802 17.4714 13.4051L23.3752 15.6163L14.5064 25.1163Z"
fill="#fff"
fill="currentColor"
/>
</svg>
),
@ -205,8 +205,7 @@ export const postSelector = (date: dayjs.Dayjs): ICommand => ({
suffix: state.command.suffix,
});
let state1 = api.setSelectionRange(newSelectionRange);
state1 = api.setSelectionRange(newSelectionRange);
const state1 = api.setSelectionRange(newSelectionRange);
const media = await showPostSelector(date);
executeCommand({
api,

View File

@ -0,0 +1,28 @@
import removeMd from 'remove-markdown';
import { makeId } from '../../../nestjs-libraries/src/services/make.is';
export const removeMarkdown = (params: { text: string; except?: RegExp[] }) => {
let modifiedText = params.text;
const except = params.except || [];
const placeholders: { [key: string]: string } = {};
// Step 2: Replace exceptions with placeholders
except.forEach((regexp, index) => {
modifiedText = modifiedText.replace(regexp, (match) => {
const placeholder = `[[EXCEPT_PLACEHOLDER_${makeId(5)}]]`;
placeholders[placeholder] = match;
return placeholder;
});
});
// Step 3: Remove markdown from modified text
// Assuming removeMd is the function that removes markdown
const cleanedText = removeMd(modifiedText);
// Step 4: Replace placeholders with original text
const finalText = Object.keys(placeholders).reduce((text, placeholder) => {
return text.replace(placeholder, placeholders[placeholder]);
}, cleanedText);
return finalText;
};

View File

@ -9,6 +9,7 @@ import sharp from 'sharp';
import { lookup } from 'mime-types';
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
import removeMd from 'remove-markdown';
import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown';
export class LinkedinProvider implements SocialProvider {
identifier = 'linkedin';
@ -114,6 +115,38 @@ export class LinkedinProvider implements SocialProvider {
};
}
async company(token: string, data: { url: string }) {
const { url } = data;
const getCompanyVanity = url.match(
/^https?:\/\/?www\.?linkedin\.com\/company\/([^/]+)\/$/
);
if (!getCompanyVanity || !getCompanyVanity?.length) {
throw new Error('Invalid LinkedIn company URL');
}
const { elements } = await (
await fetch(
`https://api.linkedin.com/rest/organizations?q=vanityName&vanityName=${getCompanyVanity[1]}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0',
'LinkedIn-Version': '202402',
Authorization: `Bearer ${token}`,
},
}
)
).json();
return {
options: elements.map((e: { localizedName: string; id: string }) => ({
label: e.localizedName,
value: `@[${e.localizedName}](urn:li:organization:${e.id})`,
}))?.[0],
};
}
private async uploadPicture(
accessToken: string,
personId: string,
@ -203,9 +236,10 @@ export class LinkedinProvider implements SocialProvider {
},
body: JSON.stringify({
author: `urn:li:person:${id}`,
commentary: removeMd(
firstPost.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')
).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
commentary: removeMarkdown({
text: firstPost.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
}).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
@ -238,6 +272,10 @@ export class LinkedinProvider implements SocialProvider {
}),
});
if (data.status !== 201 && data.status !== 200) {
throw new Error('Error posting to LinkedIn');
}
const topPostId = data.headers.get('x-restli-id')!;
const ids = [
{
@ -263,10 +301,10 @@ export class LinkedinProvider implements SocialProvider {
actor: `urn:li:person:${id}`,
object: topPostId,
message: {
text: removeMd(post.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')).replace(
'𝔫𝔢𝔴𝔩𝔦𝔫𝔢',
'\n'
),
text: removeMarkdown({
text: post.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
}).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
},
}),
}