Merge pull request #842 from gitroomhq/feat/new-modal

Refactor: new postiz creation modal
This commit is contained in:
Nevo David 2025-06-26 20:20:08 +07:00 committed by GitHub
commit 493f0f1ab4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 2686 additions and 2400 deletions

View File

@ -99,6 +99,7 @@ html {
cursor: text;
display: flex;
align-items: center;
margin-right: 0 !important;
}
.react-tags.is-active {
@ -191,6 +192,7 @@ html {
.react-tags__combobox-input {
/* prevent autoresize overflowing the container */
max-width: 100%;
width: 100% !important;
/* remove styles and layout from this element */
margin: 0;
padding: 0;
@ -198,7 +200,7 @@ html {
outline: none;
background: none;
/* match the font styles */
font-size: inherit;
font-size: 16px;
line-height: inherit;
}
@ -434,7 +436,7 @@ div div .set-font-family {
}
.tags-top .react-tags__combobox {
height: 35px;
height: 44px;
display: flex;
background-color: #141c2c;
padding-left: 10px;

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import Loading from 'react-loading';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
const list = [
'Realistic',
'Cartoon',
@ -27,10 +28,12 @@ export const AiImage: FC<{
const t = useT();
const { value, onChange } = props;
const [loading, setLoading] = useState(false);
const setLocked = useLaunchStore(p => p.setLocked);
const fetch = useFetch();
const generateImage = useCallback(
(type: string) => async () => {
setLoading(true);
setLocked(true);
const image = await (
await fetch('/media/generate-image-with-prompt', {
method: 'POST',
@ -49,6 +52,7 @@ ${type}
})
).json();
setLoading(false);
setLocked(false);
onChange(image);
},
[value, onChange]

View File

@ -31,7 +31,6 @@ import 'dayjs/locale/tr';
import 'dayjs/locale/vi';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useModals } from '@mantine/modals';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import clsx from 'clsx';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
@ -53,6 +52,7 @@ import { useInterval } from '@mantine/hooks';
import { StatisticsModal } from '@gitroom/frontend/components/launches/statistics';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import i18next from 'i18next';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
// Extend dayjs with necessary plugins
extend(isSameOrAfter);

View File

@ -1,123 +0,0 @@
import { forwardRef, useCallback, useRef, useState } from 'react';
import type { MDEditorProps } from '@uiw/react-md-editor/src/Types';
import { RefMDEditor } from '@uiw/react-md-editor/src/Editor';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { CopilotTextarea } from '@copilotkit/react-textarea';
import clsx from 'clsx';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { Transforms } from 'slate';
import EmojiPicker from 'emoji-picker-react';
import { Theme } from 'emoji-picker-react';
import { BoldText } from '@gitroom/frontend/components/launches/bold.text';
import { UText } from '@gitroom/frontend/components/launches/u.text';
import { SignatureBox } from '@gitroom/frontend/components/signature';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Editor = forwardRef<
RefMDEditor,
MDEditorProps & {
order: number;
totalPosts: number;
disabledCopilot?: boolean;
}
>(
(
props: MDEditorProps & {
order: number;
totalPosts: number;
disabledCopilot?: boolean;
},
ref: React.ForwardedRef<RefMDEditor>
) => {
const user = useUser();
const [id] = useState(makeId(10));
const newRef = useRef<any>(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const t = useT();
useCopilotReadable({
...(props.disabledCopilot ? { available: 'disabled' } : {}),
description: 'Content of the post number ' + (props.order + 1),
value: JSON.stringify({
content: props.value,
order: props.order,
allowAddContent: props?.value?.length === 0,
}),
});
useCopilotAction({
...(props.disabledCopilot ? { available: 'disabled' } : {}),
name: 'editPost_' + props.order,
description: `Edit the content of post number ${props.order}`,
parameters: [
{
name: 'content',
type: 'string',
},
],
handler: async ({ content }) => {
props?.onChange?.(content);
},
});
const addText = useCallback(
(emoji: string) => {
setTimeout(() => {
// @ts-ignore
Transforms.insertText(newRef?.current?.editor!, emoji);
}, 10);
},
[props.value, id]
);
return (
<>
<div className="flex gap-[5px] justify-end -mt-[30px]">
<SignatureBox editor={newRef?.current?.editor!} />
<UText
editor={newRef?.current?.editor!}
currentValue={props.value!}
/>
<BoldText
editor={newRef?.current?.editor!}
currentValue={props.value!}
/>
<div
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
>
{t('', '\uD83D\uDE00')}
</div>
</div>
<div className="absolute z-[200] end-0">
<EmojiPicker
theme={(localStorage.getItem('mode') as Theme) || Theme.DARK}
onEmojiClick={(e) => {
addText(e.emoji);
setEmojiPickerOpen(false);
}}
open={emojiPickerOpen}
/>
</div>
<div className="relative bg-customColor2" id={id}>
<CopilotTextarea
disableBranding={true}
ref={newRef}
className={clsx(
'!min-h-40 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none',
props.totalPosts > 1 && '!max-h-80'
)}
value={props.value}
onChange={(e) => {
props?.onChange?.(e.target.value);
}}
onPaste={props.onPaste}
placeholder={t('write_your_reply', 'Write your reply...')}
autosuggestionsConfig={{
textareaPurpose: `Assist me in writing social media posts.`,
chatApiConfigs: {},
disabled: !user?.tier?.ai,
}}
/>
</div>
</>
);
}
);

View File

@ -6,10 +6,13 @@ import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
import { FC } from 'react';
import { textSlicer } from '@gitroom/helpers/utils/count.length';
import interClass from '@gitroom/react/helpers/inter.font';
import Image from 'next/image';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
export const GeneralPreviewComponent: FC<{
maximumCharacters?: number;
}> = (props) => {
const { value: topValue, integration } = useIntegration();
const current = useLaunchStore((state) => state.current);
const mediaDir = useMediaDirectory();
const newValues = useFormatting(topValue, {
removeMarkdown: true,
@ -41,11 +44,23 @@ export const GeneralPreviewComponent: FC<{
)}
>
<div className="w-[40px] flex flex-col items-center">
<img
src={integration?.picture || '/no-picture.jpg'}
alt="x"
className="rounded-full relative z-[2]"
/>
<div className="relative">
<img
src={current === 'global' ? '/no-picture.jpg' : (integration?.picture || '/no-picture.jpg')}
alt="x"
className="rounded-full relative z-[2]"
/>
{current !== 'global' && (
<Image
src={`/icons/platforms/${integration?.identifier}.png`}
className="rounded-full absolute z-10 -bottom-[5px] -end-[5px] border border-fifth"
alt={integration.identifier}
width={20}
height={20}
/>
)}
</div>
{index !== topValue.length - 1 && (
<div className="flex-1 w-[2px] h-[calc(100%-10px)] bg-customColor25 absolute top-[10px] z-[1]" />
)}
@ -53,7 +68,7 @@ export const GeneralPreviewComponent: FC<{
<div className="flex-1 flex flex-col gap-[4px]">
<div className="flex">
<div className="h-[22px] text-[15px] font-[700]">
{integration?.name}
{current === 'global' ? 'Global Edit' : integration?.name}
</div>
<div className="text-[15px] text-customColor26 mt-[1px] ms-[2px]">
<svg
@ -69,7 +84,7 @@ export const GeneralPreviewComponent: FC<{
</svg>
</div>
<div className="text-[15px] font-[400] text-customColor27 ms-[4px]">
{integration?.display || '@username'}
{current === 'global' ? '' : integration?.display || '@username'}
</div>
</div>
<pre

View File

@ -15,10 +15,10 @@ import {
CalendarWeekProvider,
useCalendar,
} from '@gitroom/frontend/components/launches/calendar.context';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import dayjs from 'dayjs';
import { Select } from '@gitroom/react/form/select';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
const FirstStep: FC = (props) => {
const { integrations, reloadCalendarView } = useCalendar();
const modal = useModals();

View File

@ -15,6 +15,7 @@ import { Input } from '@gitroom/react/form/input';
import { Button } from '@gitroom/react/form/button';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
const postUrlEmitter = new EventEmitter();
export const ShowLinkedinCompany = () => {
const [showPostSelector, setShowPostSelector] = useState(false);
@ -53,6 +54,37 @@ export const ShowLinkedinCompany = () => {
<LinkedinCompany id={id} onClose={close} onSelect={callback?.callback!} />
);
};
export const LinkedinCompanyPop: FC<{
addText: (value: any) => void;
}> = (props) => {
const current = useLaunchStore((state) => state.current);
return (
<svg
onClick={() => {
postUrlEmitter.emit('show', {
id: current,
callback: (value: any) => {
props.addText(value);
},
});
}}
data-tooltip-id="tooltip"
data-tooltip-content="Add a LinkedIn Company"
className="mx-[10px] cursor-pointer"
width="20"
height="20"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M24 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585786C0.210714 0.960859 0 1.46957 0 2V24C0 24.5304 0.210714 25.0391 0.585786 25.4142C0.960859 25.7893 1.46957 26 2 26H24C24.5304 26 25.0391 25.7893 25.4142 25.4142C25.7893 25.0391 26 24.5304 26 24V2C26 1.46957 25.7893 0.960859 25.4142 0.585786C25.0391 0.210714 24.5304 0 24 0ZM9 19C9 19.2652 8.89464 19.5196 8.70711 19.7071C8.51957 19.8946 8.26522 20 8 20C7.73478 20 7.48043 19.8946 7.29289 19.7071C7.10536 19.5196 7 19.2652 7 19V11C7 10.7348 7.10536 10.4804 7.29289 10.2929C7.48043 10.1054 7.73478 10 8 10C8.26522 10 8.51957 10.1054 8.70711 10.2929C8.89464 10.4804 9 10.7348 9 11V19ZM8 9C7.70333 9 7.41332 8.91203 7.16665 8.7472C6.91997 8.58238 6.72771 8.34811 6.61418 8.07403C6.50065 7.79994 6.47094 7.49834 6.52882 7.20736C6.5867 6.91639 6.72956 6.64912 6.93934 6.43934C7.14912 6.22956 7.41639 6.0867 7.70736 6.02882C7.99834 5.97094 8.29994 6.00065 8.57403 6.11418C8.84811 6.22771 9.08238 6.41997 9.2472 6.66665C9.41203 6.91332 9.5 7.20333 9.5 7.5C9.5 7.89782 9.34196 8.27936 9.06066 8.56066C8.77936 8.84196 8.39782 9 8 9ZM20 19C20 19.2652 19.8946 19.5196 19.7071 19.7071C19.5196 19.8946 19.2652 20 19 20C18.7348 20 18.4804 19.8946 18.2929 19.7071C18.1054 19.5196 18 19.2652 18 19V14.5C18 13.837 17.7366 13.2011 17.2678 12.7322C16.7989 12.2634 16.163 12 15.5 12C14.837 12 14.2011 12.2634 13.7322 12.7322C13.2634 13.2011 13 13.837 13 14.5V19C13 19.2652 12.8946 19.5196 12.7071 19.7071C12.5196 19.8946 12.2652 20 12 20C11.7348 20 11.4804 19.8946 11.2929 19.7071C11.1054 19.5196 11 19.2652 11 19V11C11.0012 10.7551 11.0923 10.5191 11.256 10.3369C11.4197 10.1546 11.6446 10.0388 11.888 10.0114C12.1314 9.98392 12.3764 10.0468 12.5765 10.188C12.7767 10.3292 12.918 10.539 12.9738 10.7775C13.6502 10.3186 14.4389 10.0526 15.2552 10.0081C16.0714 9.96368 16.8844 10.1424 17.6067 10.5251C18.329 10.9078 18.9335 11.48 19.3551 12.1803C19.7768 12.8806 19.9997 13.6825 20 14.5V19Z"
fill="currentColor"
/>
</svg>
);
};
export const showPostSelector = (id: string) => {
return new Promise<string>((resolve) => {
postUrlEmitter.emit('show', {

View File

@ -1,12 +1,12 @@
import React, { useCallback } from 'react';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import { useModals } from '@mantine/modals';
import dayjs from 'dayjs';
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { SetSelectionModal } from '@gitroom/frontend/components/launches/calendar';
import { useSet } from '@gitroom/frontend/components/launches/set.context';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
export const NewPost = () => {
const fetch = useFetch();
const modal = useModals();

View File

@ -1,82 +0,0 @@
'use client';
import { FC, useEffect } from 'react';
import { CustomSelect } from '@gitroom/react/form/custom.select';
import { FormProvider, useForm } from 'react-hook-form';
export interface Information {
buyer: Buyer;
usedIds: Array<{
id: string;
status: 'NO' | 'WAITING_CONFIRMATION' | 'YES';
}>;
id: string;
missing: Missing[];
}
export interface Buyer {
id: string;
name: string;
picture: Picture;
}
export interface Picture {
id: string;
path: string;
}
export interface Missing {
integration: Integration;
missing: number;
}
export interface Integration {
quantity: number;
integration: Integration2;
}
export interface Integration2 {
id: string;
name: string;
providerIdentifier: string;
}
export const PostToOrganization: FC<{
information: Information[];
onChange: (order?: Information) => void;
selected?: string;
}> = (props) => {
const { information, onChange, selected } = props;
const form = useForm();
const postFor = form.watch('post_for');
useEffect(() => {
onChange(information?.find((p) => p.id === postFor?.value)!);
}, [postFor]);
useEffect(() => {
if (!selected || !information?.length) {
return;
}
const findIt = information?.find((p) => p.id === selected);
form.setValue('post_for', {
value: findIt?.id,
});
onChange(information?.find((p) => p.id === selected)!);
}, [selected, information]);
if (!information?.length) {
return null;
}
return (
<FormProvider {...form}>
<CustomSelect
className="w-[240px]"
removeError={true}
label=""
placeholder="Select order from marketplace"
name="post_for"
options={information?.map((p) => ({
label: 'For: ' + p?.buyer?.name,
value: p?.id,
icon: (
<img
src={p?.buyer?.picture?.path}
className="w-[24px] h-[24px] rounded-full"
/>
),
}))}
/>
</FormProvider>
);
};

View File

@ -1,54 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { ShowAllProviders } from '@gitroom/frontend/components/launches/providers/show.all.providers';
import dayjs from 'dayjs';
import { useStateCallback } from '@gitroom/react/helpers/use.state.callback';
export const ProvidersOptions: FC<{
hideEditOnlyThis: boolean;
integrations: Integrations[];
allIntegrations: Integrations[];
editorValue: Array<{
id?: string;
content: string;
}>;
date: dayjs.Dayjs;
}> = (props) => {
const { integrations, editorValue, date, hideEditOnlyThis } = props;
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback([
integrations[0],
]);
useEffect(() => {
if (integrations.indexOf(selectedIntegrations[0]) === -1) {
setSelectedIntegrations([integrations[0]]);
}
}, [integrations, selectedIntegrations]);
return (
<div className="flex flex-1 flex-col">
<PickPlatforms
integrations={integrations}
selectedIntegrations={selectedIntegrations}
onChange={setSelectedIntegrations}
singleSelect={true}
hide={integrations.length === 1}
isMain={false}
/>
<IntegrationContext.Provider
value={{
value: editorValue,
integration: selectedIntegrations?.[0],
date,
allIntegrations: props.allIntegrations,
}}
>
<ShowAllProviders
hideEditOnlyThis={hideEditOnlyThis}
value={editorValue}
integrations={integrations}
selectedProvider={selectedIntegrations?.[0]}
/>
</IntegrationContext.Provider>
</div>
);
};

View File

@ -1,8 +0,0 @@
import { InstagramContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/instagram/instagram.continue';
import { FacebookContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/facebook/facebook.continue';
import { LinkedinContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/linkedin/linkedin.continue';
export const continueProviderList = {
instagram: InstagramContinue,
facebook: FacebookContinue,
'linkedin-page': LinkedinContinue,
};

View File

@ -1,2 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(null, undefined, undefined, undefined, 63206);

View File

@ -1,707 +0,0 @@
'use client';
import React, {
FC,
Fragment,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
ClipboardEvent,
memo,
} from 'react';
import { Button } from '@gitroom/react/form/button';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor';
import { useValues } from '@gitroom/frontend/components/launches/helpers/use.values';
import { FormProvider } from 'react-hook-form';
import { useMoveToIntegrationListener } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import {
IntegrationContext,
useIntegration,
} from '@gitroom/frontend/components/launches/helpers/use.integration';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { createPortal } from 'react-dom';
import clsx from 'clsx';
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,
linkedinCompany,
} from '@gitroom/frontend/components/launches/helpers/linkedin.component';
import { Editor } from '@gitroom/frontend/components/launches/editor';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.button';
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
import { capitalize } from 'lodash';
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { InternalChannels } from '@gitroom/frontend/components/launches/internal.channels';
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useSet } from '@gitroom/frontend/components/launches/set.context';
import { SeparatePost } from '@gitroom/frontend/components/launches/separate.post';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
// Simple component to change back to settings on after changing tab
export const SetTab: FC<{
changeTab: () => void;
}> = (props) => {
useEffect(() => {
return () => {
setTimeout(() => {
props.changeTab();
}, 500);
};
}, []);
return null;
};
// This is a simple function that if we edit in place, we hide the editor on top
export const EditorWrapper: FC<{
children: ReactNode;
}> = ({ children }) => {
const showHide = useHideTopEditor();
const [showEditor, setShowEditor] = useState(false);
useEffect(() => {
setShowEditor(true);
showHide.hide();
return () => {
showHide.show();
setShowEditor(false);
};
}, []);
if (!showEditor) {
return null;
}
return children;
};
export const withProvider = function <T extends object>(
SettingsComponent: FC<{
values?: any;
}> | null,
CustomPreviewComponent?: FC<{
maximumCharacters?: number;
}>,
dto?: any,
checkValidity?: (
value: Array<
Array<{
path: string;
}>
>,
settings: T,
additionalSettings: any,
) => Promise<string | true>,
maximumCharacters?: number | ((settings: any) => number)
) {
return (props: {
identifier: string;
id: string;
value: Array<{
content: string;
id?: string;
image?: Array<{
path: string;
id: string;
}>;
}>;
hideMenu?: boolean;
show: boolean;
hideEditOnlyThis?: boolean;
}) => {
const existingData = useExistingData();
const t = useT();
const { allIntegrations, integration, date } = useIntegration();
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
const [uploading, setUploading] = useState(false);
const [showComponent, setShowComponent] = useState(false);
const fetch = useFetch();
useEffect(() => {
setTimeout(() => {
setShowComponent(true);
}, 1);
}, []);
useCopilotReadable({
description:
integration?.type === 'social'
? 'force content always in MD format'
: 'force content always to be fit to social media',
value: '',
});
const [editInPlace, setEditInPlace] = useState(!!existingData.integration);
const [InPlaceValue, setInPlaceValue] = useState<
Array<{
id?: string;
content: string;
image?: Array<{
id: string;
path: string;
}>;
}>
>(
// @ts-ignore
existingData.integration
? existingData.posts.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))
: [
{
content: '',
},
]
);
const [showTab, setShowTab] = useState(0);
const Component = useMemo(() => {
return SettingsComponent ? SettingsComponent : () => <></>;
}, [SettingsComponent]);
// in case there is an error on submit, we change to the settings tab for the specific provider
useMoveToIntegrationListener(
[props.id],
true,
({ identifier, toPreview }) => {
if (identifier === props.id) {
setShowTab(toPreview ? 1 : 2);
}
}
);
const set = useSet();
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
const form = useValues(
set?.set
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)
?.settings
: existingData.settings,
props.id,
props.identifier,
editInPlace ? InPlaceValue : props.value,
dto,
checkValidity,
!maximumCharacters
? undefined
: typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(JSON.parse(integration?.additionalSettings || '[]'))
);
// change editor value
const changeValue = useCallback(
(index: number) => (newValue: string) => {
return setInPlaceValue((prev) => {
prev[index].content = newValue;
return [...prev];
});
},
[InPlaceValue]
);
const merge = useCallback(() => {
setInPlaceValue(
InPlaceValue.reduce(
(all, current) => {
all[0].content = all[0].content + current.content + '\n';
all[0].image = [...all[0].image, ...(current.image || [])];
return all;
},
[
{
content: '',
id: InPlaceValue[0].id,
image: [] as {
id: string;
path: string;
}[],
},
]
)
);
}, [InPlaceValue]);
const separatePosts = useCallback(
(posts: string[]) => {
setInPlaceValue(
posts.map((p, i) => ({
content: p,
id: InPlaceValue?.[i]?.id || makeId(10),
image: InPlaceValue?.[i]?.image || [],
}))
);
},
[InPlaceValue]
);
const changeImage = useCallback(
(index: number) =>
(newValue: {
target: {
name: string;
value?: Array<{
id: string;
path: string;
}>;
};
}) => {
return setInPlaceValue((prev) => {
prev[index].image = newValue.target.value;
return [...prev];
});
},
[InPlaceValue]
);
// add another local editor
const addValue = useCallback(
(index: number) => () => {
setInPlaceValue((prev) => {
return prev.reduce(
(acc, p, i) => {
acc.push(p);
if (i === index) {
acc.push({
content: '',
});
}
return acc;
},
[] as Array<{
content: string;
}>
);
});
},
[]
);
const changePosition = useCallback(
(index: number) => (type: 'up' | 'down') => {
if (type === 'up' && index !== 0) {
setInPlaceValue((prev) => {
return arrayMoveImmutable(prev, index, index - 1);
});
} else if (type === 'down') {
setInPlaceValue((prev) => {
return arrayMoveImmutable(prev, index, index + 1);
});
}
},
[]
);
// Delete post
const deletePost = useCallback(
(index: number) => async () => {
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
return;
}
setInPlaceValue((prev) => {
prev.splice(index, 1);
return [...prev];
});
},
[InPlaceValue]
);
// This is a function if we want to switch from the global editor to edit in place
const changeToEditor = useCallback(async () => {
if (
!(await deleteDialog(
!editInPlace
? 'Are you sure you want to edit only this?'
: 'Are you sure you want to revert it back to global editing?',
'Yes, edit in place!'
))
) {
return false;
}
setEditInPlace(!editInPlace);
setInPlaceValue(
editInPlace
? [
{
content: '',
},
]
: props.value.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))
);
}, [props.value, editInPlace]);
useCopilotAction({
name: editInPlace
? 'switchToGlobalEdit'
: `editInPlace_${integration?.identifier}`,
description: editInPlace
? 'Switch to global editing'
: `Edit only ${integration?.identifier} this, if you want a different identifier, you have to use setSelectedIntegration first`,
handler: async () => {
await changeToEditor();
},
});
const tagPersonOrCompany = useCallback(
(integration: string, editor: (value: string) => void) => () => {
setShowLinkedinPopUp(
<LinkedinCompany
onSelect={(tag) => {
editor(tag);
}}
id={integration}
onClose={() => setShowLinkedinPopUp(false)}
/>
);
},
[]
);
const uppy = useUppyUploader({
onUploadSuccess: () => {
/**empty**/
},
allowedFileTypes: 'image/*,video/mp4',
});
const pasteImages = useCallback(
(index: number, currentValue: any[], isFile?: boolean) => {
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
// @ts-ignore
const clipboardItems = isFile
? // @ts-ignore
event.map((p) => ({
kind: 'file',
getAsFile: () => p,
}))
: // @ts-ignore
event.clipboardData?.items; // Ensure clipboardData is available
if (!clipboardItems) {
return;
}
const files: File[] = [];
// @ts-ignore
for (const item of clipboardItems) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
if (isImage || isVideo) {
files.push(file); // Collect images or videos
}
}
}
}
if (files.length === 0) {
return;
}
setUploading(true);
const lastValues = [...currentValue];
for (const file of files) {
uppy.addFile(file);
const upload = await uppy.upload();
uppy.clear();
if (upload?.successful?.length) {
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
changeImage(index)({
target: {
name: 'image',
value: [...lastValues],
},
});
}
}
setUploading(false);
};
},
[changeImage]
);
const getInternalPlugs = useCallback(async () => {
return (
await fetch(`/integrations/${props.identifier}/internal-plugs`)
).json();
}, [props.identifier]);
const { data, isLoading } = useSWR(
`internal-${props.identifier}`,
getInternalPlugs,
{
revalidateOnReconnect: true,
}
);
// this is a trick to prevent the data from being deleted, yet we don't render the elements
if (!props.show || !showComponent || isLoading) {
return null;
}
return (
<FormProvider {...form}>
<SetTab changeTab={() => setShowTab(0)} />
{showLinkedinPopUp ? showLinkedinPopUp : null}
<div className="mt-[15px] w-full flex flex-col flex-1">
{!props.hideMenu && (
<div className="flex gap-[4px]">
<div className="flex-1 flex">
<Button
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
secondary={showTab !== 0}
onClick={() => setShowTab(0)}
>
{t('preview', 'Preview')}
</Button>
</div>
{(!!SettingsComponent || !!data?.internalPlugs?.length) && (
<div className="flex-1 flex">
<Button
className={clsx(
'flex-1 overflow-hidden whitespace-nowrap',
showTab === 2 && 'rounded-[4px]'
)}
secondary={showTab !== 2}
onClick={() => setShowTab(2)}
>
{t('settings', 'Settings')}
</Button>
</div>
)}
{!existingData.integration && !props.hideEditOnlyThis && (
<div className="flex-1 flex">
<Button
className="text-white rounded-[4px] flex-1 !bg-red-700 overflow-hidden whitespace-nowrap"
secondary={showTab !== 1}
onClick={changeToEditor}
>
{editInPlace
? 'Edit globally'
: `Edit only ${integration?.name.slice(0, 10)}${
(integration?.name?.length || 0) > 10 ? '...' : ''
} (${capitalize(
integration?.identifier.replace('-', ' ')
)})`}
</Button>
</div>
)}
</div>
)}
{editInPlace &&
createPortal(
<EditorWrapper>
{uploading && (
<div className="absolute start-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
<LoadingComponent width={100} height={100} />
</div>
)}
<div className="flex flex-col gap-[20px]">
{!existingData?.integration && (
<div className="bg-red-800 text-white">
{t(
'you_are_now_editing_only',
'You are now editing only'
)}
{integration?.name} (
{capitalize(integration?.identifier.replace('-', ' '))})
</div>
)}
{InPlaceValue.map((val, index) => (
<Fragment key={`edit_inner_${index}`}>
<div>
<div className="flex gap-[4px]">
<div className="flex-1 text-textColor editor">
{(integration?.identifier === 'linkedin' ||
integration?.identifier === 'linkedin-page') && (
<Button
className="mb-[5px]"
onClick={tagPersonOrCompany(
integration.id,
(newValue: string) =>
changeValue(index)(val.content + newValue)
)}
>
{t('tag_a_company', 'Tag a company')}
</Button>
)}
<DropFiles
onDrop={pasteImages(index, val.image || [], true)}
>
<Editor
order={index}
height={InPlaceValue.length > 1 ? 200 : 250}
value={val.content}
totalPosts={InPlaceValue.length}
commands={[
// ...commands
// .getCommands()
// .filter((f) => f.name !== 'image'),
// newImage,
postSelector(date),
...linkedinCompany(
integration?.identifier!,
integration?.id!
),
]}
preview="edit"
onPaste={pasteImages(index, val.image || [])}
// @ts-ignore
onChange={changeValue(index)}
/>
</DropFiles>
{(!val.content || val.content.length < 6) && (
<div className="my-[5px] !bg-red-600 text-[12px] font-[500]">
{t(
'the_post_should_be_at_least_6_characters_long',
'The post should be at least 6 characters long'
)}
</div>
)}
<div className="flex">
<div className="flex-1">
<MultiMediaComponent
allData={InPlaceValue}
text={val.content}
label="Attachments"
description=""
name="image"
value={val.image}
onChange={changeImage(index)}
/>
</div>
<div className="flex bg-customColor20 rounded-br-[8px] text-customColor19">
{InPlaceValue.length > 1 && (
<div
className="flex cursor-pointer gap-[4px] justify-center items-center flex-1"
onClick={deletePost(index)}
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="currentColor"
>
<path
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
fill="#F97066"
/>
</svg>
</div>
<div className="text-[12px] font-[500] pe-[10px]">
{t('delete_post', 'Delete Post')}
</div>
</div>
)}
</div>
</div>
</div>
<div>
<UpDownArrow
isUp={index !== 0}
isDown={
InPlaceValue.length !== 0 &&
InPlaceValue.length !== index + 1
}
onChange={changePosition(index)}
/>
</div>
</div>
</div>
<div>
<AddPostButton onClick={addValue(index)} num={index} />
</div>
</Fragment>
))}
<div className="flex gap-[4px]">
{InPlaceValue.length > 1 && (
<div>
<MergePost merge={merge} />
</div>
)}
<div>
<SeparatePost
changeLoading={setUploading}
posts={InPlaceValue.map((p) => p.content)}
len={
typeof maximumCharacters === 'number'
? maximumCharacters
: 10000
}
merge={separatePosts}
/>
</div>
</div>
</div>
</EditorWrapper>,
document.querySelector('#renderEditor')!
)}
{(showTab === 0 || showTab === 2) && (
<div className={clsx('mt-[20px]', showTab !== 2 && 'hidden')}>
<Component values={editInPlace ? InPlaceValue : props.value} />
{!!data?.internalPlugs?.length && (
<InternalChannels plugs={data?.internalPlugs} />
)}
</div>
)}
{showTab === 0 && (
<div className="mt-[20px] flex flex-col items-center">
<IntegrationContext.Provider
value={{
allIntegrations,
date,
value: editInPlace ? InPlaceValue : props.value,
integration,
}}
>
{(editInPlace ? InPlaceValue : props.value)
.map((p) => p.content)
.join('').length ? (
CustomPreviewComponent ? (
<CustomPreviewComponent
maximumCharacters={
!maximumCharacters
? undefined
: typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
integration?.additionalSettings || '[]'
)
)
}
/>
) : (
<GeneralPreviewComponent
maximumCharacters={
!maximumCharacters
? undefined
: typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
integration?.additionalSettings || '[]'
)
)
}
/>
)
) : (
<>{t('no_content_yet', 'No Content Yet')}</>
)}
</IntegrationContext.Provider>
</div>
)}
</div>
</FormProvider>
);
};
};

View File

@ -1,3 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(null, undefined, undefined, undefined, 500);

View File

@ -1,10 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async () => {
return true;
},
undefined
);

View File

@ -1,159 +0,0 @@
import { FC } from 'react';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import DevtoProvider from '@gitroom/frontend/components/launches/providers/devto/devto.provider';
import XProvider from '@gitroom/frontend/components/launches/providers/x/x.provider';
import LinkedinProvider from '@gitroom/frontend/components/launches/providers/linkedin/linkedin.provider';
import RedditProvider from '@gitroom/frontend/components/launches/providers/reddit/reddit.provider';
import MediumProvider from '@gitroom/frontend/components/launches/providers/medium/medium.provider';
import HashnodeProvider from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider';
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.collaborators';
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';
import ThreadsProvider from '@gitroom/frontend/components/launches/providers/threads/threads.provider';
import DiscordProvider from '@gitroom/frontend/components/launches/providers/discord/discord.provider';
import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider';
import MastodonProvider from '@gitroom/frontend/components/launches/providers/mastodon/mastodon.provider';
import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider';
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
import VkProvider from '@gitroom/frontend/components/launches/providers/vk/vk.provider';
export const Providers = [
{
identifier: 'devto',
component: DevtoProvider,
},
{
identifier: 'x',
component: XProvider,
},
{
identifier: 'linkedin',
component: LinkedinProvider,
},
{
identifier: 'linkedin-page',
component: LinkedinProvider,
},
{
identifier: 'reddit',
component: RedditProvider,
},
{
identifier: 'medium',
component: MediumProvider,
},
{
identifier: 'hashnode',
component: HashnodeProvider,
},
{
identifier: 'facebook',
component: FacebookProvider,
},
{
identifier: 'instagram',
component: InstagramProvider,
},
{
identifier: 'instagram-standalone',
component: InstagramProvider,
},
{
identifier: 'youtube',
component: YoutubeProvider,
},
{
identifier: 'tiktok',
component: TiktokProvider,
},
{
identifier: 'pinterest',
component: PinterestProvider,
},
{
identifier: 'dribbble',
component: DribbbleProvider,
},
{
identifier: 'threads',
component: ThreadsProvider,
},
{
identifier: 'discord',
component: DiscordProvider,
},
{
identifier: 'slack',
component: SlackProvider,
},
{
identifier: 'mastodon',
component: MastodonProvider,
},
{
identifier: 'bluesky',
component: BlueskyProvider,
},
{
identifier: 'lemmy',
component: LemmyProvider,
},
{
identifier: 'wrapcast',
component: WarpcastProvider,
},
{
identifier: 'telegram',
component: TelegramProvider,
},
{
identifier: 'nostr',
component: NostrProvider,
},
{
identifier: 'vk',
component: VkProvider,
},
];
export const ShowAllProviders: FC<{
integrations: Integrations[];
hideEditOnlyThis: boolean;
value: Array<{
content: string;
id?: string;
}>;
selectedProvider?: Integrations;
}> = (props) => {
const { integrations, value, selectedProvider, hideEditOnlyThis } = props;
return (
<>
{integrations.map((integration) => {
const { component: ProviderComponent } = Providers.find(
(provider) => provider.identifier === integration.identifier
) || {
component: null,
};
if (
!ProviderComponent ||
integrations.map((p) => p.id).indexOf(selectedProvider?.id!) === -1
) {
return null;
}
return (
<ProviderComponent
hideEditOnlyThis={hideEditOnlyThis}
key={integration.id}
{...integration}
value={value}
show={selectedProvider?.id === integration.id}
/>
);
})}
</>
);
};

View File

@ -1,10 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async () => {
return true;
},
4096
);

View File

@ -1,15 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher';
const SettingsComponent = () => {
return <ThreadFinisher />;
};
export default withProvider(
SettingsComponent,
undefined,
undefined,
async () => {
return true;
},
500
);

View File

@ -1,10 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async (posts) => {
return true;
},
2048
);

View File

@ -1,41 +0,0 @@
import React, { FC, ReactNode, useCallback } from 'react';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Submitted: FC<{
children: ReactNode;
postId: string;
status: 'YES' | 'NO' | 'WAITING_CONFIRMATION';
updateOrder: () => void;
}> = (props) => {
const { postId, updateOrder, status, children } = props;
const fetch = useFetch();
const t = useT();
const cancel = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to cancel this publication?',
'Yes'
))
) {
return;
}
await fetch(`/marketplace/posts/${postId}/cancel`, {
method: 'POST',
});
updateOrder();
}, [postId]);
if (!status || status === 'NO') {
return <>{children}</>;
}
return (
<Button
className="rounded-[4px] border-2 border-red-400 text-red-400"
secondary={true}
onClick={cancel}
>
{t('cancel_publication', 'Cancel publication')}
</Button>
);
};

View File

@ -1,11 +1,11 @@
import React, { FC, useCallback, useMemo } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { continueProviderList } from '@gitroom/frontend/components/launches/providers/continue-provider/list';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import dayjs from 'dayjs';
import useSWR, { useSWRConfig } from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { continueProviderList } from '@gitroom/frontend/components/new-launch/providers/continue-provider/list';
export const Null: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -1,7 +1,7 @@
import 'reflect-metadata';
import { FC } from 'react';
import { Post as PrismaPost } from '.prisma/client';
import { Providers } from '@gitroom/frontend/components/launches/providers/show.all.providers';
import { Providers } from '@gitroom/frontend/components/new-launch/providers/show.all.providers';
export const PreviewPopupDynamic: FC<{
postId: string;
providerId: string;
@ -15,17 +15,5 @@ export const PreviewPopupDynamic: FC<{
const { component: ProviderComponent } = Providers.find(
(p) => p.identifier === props.providerId
)!;
return (
<ProviderComponent
hideMenu={true}
show={true}
identifier={props.post.integration}
// @ts-ignore
value={props.post.posts.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))}
/>
);
return null;
};

View File

@ -0,0 +1,175 @@
'use client';
import 'reflect-metadata';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import dayjs from 'dayjs';
import type { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { FC, useEffect } from 'react';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { ManageModal } from '@gitroom/frontend/components/new-launch/manage.modal';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import { useShallow } from 'zustand/react/shallow';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
export interface AddEditModalProps {
date: dayjs.Dayjs;
integrations: Integrations[];
allIntegrations?: Integrations[];
set?: CreatePostDto;
addEditSets?: (data: any) => void;
reopenModal: () => void;
mutate: () => void;
padding?: string;
customClose?: () => void;
onlyValues?: Array<{
content: string;
id?: string;
image?: Array<{
id: string;
path: string;
}>;
}>;
}
export const AddEditModal: FC<AddEditModalProps> = (props) => {
const { setAllIntegrations, setDate, setIsCreateSet } = useLaunchStore(
useShallow((state) => ({
setAllIntegrations: state.setAllIntegrations,
setDate: state.setDate,
setIsCreateSet: state.setIsCreateSet,
}))
);
const integrations = useLaunchStore((state) => state.integrations);
useEffect(() => {
setDate(props.date || dayjs());
setAllIntegrations(
(props.integrations || []).filter((f) => !f.inBetweenSteps && !f.disabled)
);
setIsCreateSet(!!props.addEditSets);
}, []);
if (!integrations.length) {
return null;
}
return <AddEditModalInner {...props} />;
};
export const AddEditModalInner: FC<AddEditModalProps> = (props) => {
const existingData = useExistingData();
const { addOrRemoveSelectedIntegration, selectedIntegrations, integrations } =
useLaunchStore(
useShallow((state) => ({
integrations: state.integrations,
selectedIntegrations: state.selectedIntegrations,
addOrRemoveSelectedIntegration: state.addOrRemoveSelectedIntegration,
}))
);
useEffect(() => {
if (props?.set?.posts?.length) {
for (const post of props?.set?.posts) {
if (post.integration) {
const integration = integrations.find(
(i) => i.id === post.integration.id
);
addOrRemoveSelectedIntegration(integration, post.settings);
}
}
}
if (existingData.integration) {
const integration = integrations.find(
(i) => i.id === existingData.integration
);
addOrRemoveSelectedIntegration(integration, existingData.settings);
}
}, []);
if (existingData.integration && selectedIntegrations.length === 0) {
return null;
}
return <AddEditModalInnerInner {...props} />;
};
export const AddEditModalInnerInner: FC<AddEditModalProps> = (props) => {
const existingData = useExistingData();
const {
reset,
addGlobalValue,
addInternalValue,
global,
setCurrent,
internal,
setTags,
} = useLaunchStore(
useShallow((state) => ({
reset: state.reset,
addGlobalValue: state.addGlobalValue,
addInternalValue: state.addInternalValue,
setCurrent: state.setCurrent,
global: state.global,
internal: state.internal,
setTags: state.setTags,
}))
);
useEffect(() => {
if (existingData.integration) {
setTags(
// @ts-ignore
existingData?.posts?.[0]?.tags?.map((p: any) => ({
label: p.tag.name,
value: p.tag.name,
})) || []
);
addInternalValue(
0,
existingData.integration,
existingData.posts.map((post) => ({
content: post.content,
id: post.id,
// @ts-ignore
media: post.image as any[],
}))
);
setCurrent(existingData.integration);
}
addGlobalValue(
0,
props.onlyValues?.length
? props.onlyValues.map((p) => ({
content: p.content,
id: makeId(10),
media: p.image || [],
}))
: props.set?.posts?.length
? props.set.posts[0].value.map((p) => ({
id: makeId(10),
content: p.content,
// @ts-ignore
media: p.media,
}))
: [
{
content: '',
id: makeId(10),
media: [],
},
]
);
return () => {
reset();
};
}, []);
if (!global.length && !internal.length) {
return null;
}
return <ManageModal {...props} />;
};

View File

@ -1,6 +1,7 @@
'use client';
import { Button } from '@gitroom/react/form/button';
import React, { FC } from 'react';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const AddPostButton: FC<{
onClick: () => void;
@ -8,13 +9,7 @@ export const AddPostButton: FC<{
}> = (props) => {
const { onClick, num } = props;
const t = useT();
useCopilotAction({
name: 'addPost_' + num,
description: 'Add a post after the post number ' + num,
handler: () => {
onClick();
},
});
return (
<Button
onClick={onClick}

View File

@ -1,3 +1,5 @@
'use client';
import { FC, useCallback } from 'react';
import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
@ -88,7 +90,7 @@ export const BoldText: FC<{
return (
<div
onClick={mark}
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
>
<svg
width="25"

View File

@ -0,0 +1,452 @@
'use client';
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { CopilotTextarea } from '@copilotkit/react-textarea';
import clsx from 'clsx';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { Transforms } from 'slate';
import EmojiPicker from 'emoji-picker-react';
import { Theme } from 'emoji-picker-react';
import { BoldText } from '@gitroom/frontend/components/new-launch/bold.text';
import { UText } from '@gitroom/frontend/components/new-launch/u.text';
import { SignatureBox } from '@gitroom/frontend/components/signature';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
import { AddPostButton } from '@gitroom/frontend/components/new-launch/add.post.button';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import {
LinkedinCompany,
LinkedinCompanyPop,
ShowLinkedinCompany,
} from '@gitroom/frontend/components/launches/helpers/linkedin.component';
export const EditorWrapper: FC<{
totalPosts: number;
value: string;
}> = (props) => {
const {
setGlobalValueText,
setInternalValueText,
addRemoveInternal,
internal,
global,
current,
addInternalValue,
addGlobalValue,
setInternalValueMedia,
setGlobalValueMedia,
changeOrderGlobal,
changeOrderInternal,
isCreateSet,
deleteGlobalValue,
deleteInternalValue,
setGlobalValue,
setInternalValue,
internalFromAll,
} = useLaunchStore(
useShallow((state) => ({
internal: state.internal.find((p) => p.integration.id === state.current),
internalFromAll: state.integrations.find(p => p.id === state.current),
global: state.global,
current: state.current,
addRemoveInternal: state.addRemoveInternal,
setInternalValueText: state.setInternalValueText,
setGlobalValueText: state.setGlobalValueText,
addInternalValue: state.addInternalValue,
addGlobalValue: state.addGlobalValue,
setGlobalValueMedia: state.setGlobalValueMedia,
setInternalValueMedia: state.setInternalValueMedia,
changeOrderGlobal: state.changeOrderGlobal,
changeOrderInternal: state.changeOrderInternal,
isCreateSet: state.isCreateSet,
deleteGlobalValue: state.deleteGlobalValue,
deleteInternalValue: state.deleteInternalValue,
setGlobalValue: state.setGlobalValue,
setInternalValue: state.setInternalValue,
}))
);
const existingData = useExistingData();
const [loaded, setLoaded] = useState(true);
useEffect(() => {
if (loaded) {
return;
}
setLoaded(true);
}, [loaded]);
const canEdit = useMemo(() => {
return current === 'global' || !!internal;
}, [current, internal]);
const items = useMemo(() => {
if (internal) {
return internal.integrationValue;
}
return global;
}, [current, internal, global]);
const setValue = useCallback(
(value: string[]) => {
const newValue = value.map((p, index) => {
return {
id: makeId(10),
...(items?.[index]?.media
? { media: items[index].media }
: { media: [] }),
content: p,
};
});
if (internal) {
return setInternalValue(current, newValue);
}
return setGlobalValue(newValue);
},
[internal, items]
);
useCopilotReadable({
description: 'Current content of posts',
value: items.map((p) => p.content),
});
useCopilotAction({
name: 'setPosts',
description: 'a thread of posts',
parameters: [
{
name: 'content',
type: 'string[]',
description: 'a thread of posts',
},
],
handler: async ({ content }) => {
setValue(content);
},
});
const changeValue = useCallback(
(index: number) => (value: string) => {
if (internal) {
return setInternalValueText(current, index, value);
}
return setGlobalValueText(index, value);
},
[current, global, internal]
);
const changeImages = useCallback(
(index: number) => (value: any[]) => {
if (internal) {
return setInternalValueMedia(current, index, value);
}
return setGlobalValueMedia(index, value);
},
[current, global, internal]
);
const changeOrder = useCallback(
(index: number) => (direction: 'up' | 'down') => {
if (internal) {
changeOrderInternal(current, index, direction);
return setLoaded(false);
}
changeOrderGlobal(index, direction);
setLoaded(false);
},
[changeOrderInternal, changeOrderGlobal, current, global, internal]
);
const goBackToGlobal = useCallback(async () => {
if (
await deleteDialog(
'This action is irreversible. Are you sure you want to go back to global mode?'
)
) {
setLoaded(false);
addRemoveInternal(current);
}
}, []);
const addValue = useCallback(
(index: number) => () => {
if (internal) {
return addInternalValue(index, current, [
{
content: '',
id: makeId(10),
media: [],
},
]);
}
return addGlobalValue(index, [
{
content: '',
id: makeId(10),
media: [],
},
]);
},
[current, global, internal]
);
const deletePost = useCallback(
(index: number) => async () => {
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
return;
}
if (internal) {
deleteInternalValue(current, index);
return setLoaded(false);
}
deleteGlobalValue(index);
setLoaded(false);
},
[current, global, internal]
);
if (!loaded) {
return null;
}
return items.map((g, index) => (
<div key={g.id} className="relative flex flex-col gap-[10px]">
{!canEdit && !isCreateSet && (
<div
onClick={() => {
if (index !== 0) {
return;
}
setLoaded(false);
addRemoveInternal(current);
}}
className="select-none cursor-pointer absolute w-full h-full left-0 top-0 bg-red-600/10 z-[100]"
>
{index === 0 && (
<div className="absolute left-[50%] top-[50%] bg-red-400 -translate-x-[50%] z-[101] -translate-y-[50%] border-dashed border p-[10px] border-black">
Edit
</div>
)}
</div>
)}
<div className="flex gap-[5px]">
<div className="flex-1">
<Editor
allValues={items}
onChange={changeValue(index)}
key={index}
num={index}
totalPosts={global.length}
value={g.content}
pictures={g.media}
setImages={changeImages(index)}
autoComplete={canEdit}
validateChars={true}
identifier={internalFromAll?.identifier || 'global'}
/>
</div>
<div className="flex flex-col items-center gap-[10px]">
<UpDownArrow
isUp={index !== 0}
isDown={index !== items.length - 1}
onChange={changeOrder(index)}
/>
{index === 0 &&
current !== 'global' &&
canEdit &&
!existingData.integration && (
<svg
onClick={goBackToGlobal}
className="cursor-pointer"
data-tooltip-id="tooltip"
data-tooltip-content="Go back to global mode"
width="20"
height="20"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 3C13.4288 3 10.9154 3.76244 8.77759 5.1909C6.63975 6.61935 4.97351 8.64968 3.98957 11.0251C3.00563 13.4006 2.74819 16.0144 3.2498 18.5362C3.75141 21.0579 4.98953 23.3743 6.80762 25.1924C8.6257 27.0105 10.9421 28.2486 13.4638 28.7502C15.9856 29.2518 18.5995 28.9944 20.9749 28.0104C23.3503 27.0265 25.3807 25.3603 26.8091 23.2224C28.2376 21.0846 29 18.5712 29 16C28.9964 12.5533 27.6256 9.24882 25.1884 6.81163C22.7512 4.37445 19.4467 3.00364 16 3ZM12.7038 21H19.2963C18.625 23.2925 17.5 25.3587 16 26.9862C14.5 25.3587 13.375 23.2925 12.7038 21ZM12.25 19C11.9183 17.0138 11.9183 14.9862 12.25 13H19.75C20.0817 14.9862 20.0817 17.0138 19.75 19H12.25ZM5.00001 16C4.99914 14.9855 5.13923 13.9759 5.41626 13H10.2238C9.92542 14.9889 9.92542 17.0111 10.2238 19H5.41626C5.13923 18.0241 4.99914 17.0145 5.00001 16ZM19.2963 11H12.7038C13.375 8.7075 14.5 6.64125 16 5.01375C17.5 6.64125 18.625 8.7075 19.2963 11ZM21.7763 13H26.5838C27.1388 14.9615 27.1388 17.0385 26.5838 19H21.7763C22.0746 17.0111 22.0746 14.9889 21.7763 13ZM25.7963 11H21.3675C20.8572 8.99189 20.0001 7.0883 18.835 5.375C20.3236 5.77503 21.7119 6.48215 22.9108 7.45091C24.1097 8.41967 25.0926 9.62861 25.7963 11ZM13.165 5.375C11.9999 7.0883 11.1428 8.99189 10.6325 11H6.20376C6.90741 9.62861 7.89029 8.41967 9.08918 7.45091C10.2881 6.48215 11.6764 5.77503 13.165 5.375ZM6.20376 21H10.6325C11.1428 23.0081 11.9999 24.9117 13.165 26.625C11.6764 26.225 10.2881 25.5178 9.08918 24.5491C7.89029 23.5803 6.90741 22.3714 6.20376 21ZM18.835 26.625C20.0001 24.9117 20.8572 23.0081 21.3675 21H25.7963C25.0926 22.3714 24.1097 23.5803 22.9108 24.5491C21.7119 25.5178 20.3236 26.225 18.835 26.625Z"
fill="#ef4444"
/>
</svg>
)}
{items.length > 1 && (
<svg
onClick={deletePost(index)}
className="cursor-pointer"
data-tooltip-id="tooltip"
data-tooltip-content="Delete Post"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 14 14"
fill="currentColor"
>
<path
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
fill="#ef4444"
/>
</svg>
)}
</div>
</div>
{canEdit && <AddPostButton num={index} onClick={addValue(index)} />}
</div>
));
};
export const Editor: FC<{
totalPosts: number;
value: string;
num?: number;
pictures?: any[];
allValues?: any[];
onChange: (value: string) => void;
setImages?: (value: any[]) => void;
autoComplete?: boolean;
validateChars?: boolean;
identifier?: string;
}> = (props) => {
const {
allValues,
pictures,
setImages,
num,
autoComplete,
validateChars,
identifier,
} = props;
const user = useUser();
const [id] = useState(makeId(10));
const newRef = useRef<any>(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const t = useT();
const addText = useCallback(
(emoji: string) => {
setTimeout(() => {
// @ts-ignore
Transforms.insertText(newRef?.current?.editor!, emoji);
}, 10);
},
[props.value, id]
);
return (
<>
<div className="relative bg-customColor2" id={id}>
<div className="flex gap-[5px] bg-[#1b263b] border-b border-t border-customColor3 justify-center items-center p-[5px]">
<SignatureBox editor={newRef?.current?.editor!} />
<UText
editor={newRef?.current?.editor!}
currentValue={props.value!}
/>
<BoldText
editor={newRef?.current?.editor!}
currentValue={props.value!}
/>
<div
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
>
{'\uD83D\uDE00'}
</div>
{identifier === 'linkedin' || identifier === 'linkedin-page' ? (
<LinkedinCompanyPop addText={addText} />
) : null}
<div className="relative">
<div className="absolute z-[200] top-[35px] -start-[50px]">
<EmojiPicker
theme={(localStorage.getItem('mode') as Theme) || Theme.DARK}
onEmojiClick={(e) => {
addText(e.emoji);
setEmojiPickerOpen(false);
}}
open={emojiPickerOpen}
/>
</div>
</div>
</div>
<CopilotTextarea
disableBranding={true}
ref={newRef}
className={clsx(
'!min-h-40 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none',
props.totalPosts > 1 && '!max-h-80'
)}
value={props.value}
onChange={(e) => {
props?.onChange?.(e.target.value);
}}
// onPaste={props.onPaste}
placeholder={t('write_your_reply', 'Write your post...')}
autosuggestionsConfig={{
textareaPurpose: `Assist me in writing social media posts.`,
chatApiConfigs: {
suggestionsApiConfig: {
maxTokens: 20,
stop: ['.', '?', '!'],
},
},
disabled: user?.tier?.ai ? !autoComplete : true,
}}
/>
{validateChars && props.value.length < 6 && (
<div className="px-3 text-sm bg-red-600 text-red-300 mb-[4px]">
{t(
'the_post_should_be_at_least_6_characters_long',
'The post should be at least 6 characters long'
)}
</div>
)}
</div>
<div className="flex gap-[5px] bg-customColor2">
{setImages && (
<MultiMediaComponent
allData={allValues}
text={props.value}
label={t('attachments', 'Attachments')}
description=""
value={props.pictures}
name="image"
onChange={(value) => {
setImages(value.target.value);
}}
onOpen={() => {}}
onClose={() => {}}
/>
)}
</div>
</>
);
};

View File

@ -1,10 +1,11 @@
'use client';
import { Slider } from '@gitroom/react/form/slider';
import clsx from 'clsx';
import { useState } from 'react';
import { Editor } from '@gitroom/frontend/components/launches/editor';
import { Editor } from '@gitroom/frontend/components/new-launch/editor';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const ThreadFinisher = () => {
const integration = useIntegration();
@ -49,10 +50,7 @@ export const ThreadFinisher = () => {
<Editor
onChange={(val) => setValue('thread_finisher', val)}
value={value}
height={150}
totalPosts={1}
order={1}
preview="edit"
/>
</div>
</div>

View File

@ -0,0 +1,444 @@
'use client';
import React, { FC, useCallback, useRef, useState } from 'react';
import { AddEditModalProps } from '@gitroom/frontend/components/new-launch/add.edit.modal';
import clsx from 'clsx';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { PicksSocialsComponent } from '@gitroom/frontend/components/new-launch/picks.socials.component';
import { EditorWrapper } from '@gitroom/frontend/components/new-launch/editor';
import { SelectCurrent } from '@gitroom/frontend/components/new-launch/select.current';
import { ShowAllProviders } from '@gitroom/frontend/components/new-launch/providers/show.all.providers';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { DatePicker } from '@gitroom/frontend/components/launches/helpers/date.picker';
import { useShallow } from 'zustand/react/shallow';
import { RepeatComponent } from '@gitroom/frontend/components/launches/repeat.component';
import { TagsComponent } from '@gitroom/frontend/components/launches/tags.component';
import { Button } from '@gitroom/react/form/button';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { weightedLength } from '@gitroom/helpers/utils/count.length';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { useModals } from '@mantine/modals';
import { capitalize } from 'lodash';
import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload';
// @ts-ignore
import useKeypress from 'react-use-keypress';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
import { CopilotPopup } from '@copilotkit/react-ui';
function countCharacters(text: string, type: string): number {
if (type !== 'x') {
return text.length;
}
return weightedLength(text);
}
export const ManageModal: FC<AddEditModalProps> = (props) => {
const t = useT();
const fetch = useFetch();
const ref = useRef(null);
const existingData = useExistingData();
const [loading, setLoading] = useState(false);
const toaster = useToaster();
const modal = useModals();
usePreventWindowUnload(true);
const { addEditSets, mutate, customClose } = props;
const {
selectedIntegrations,
hide,
date,
setDate,
repeater,
setRepeater,
tags,
setTags,
integrations,
setSelectedIntegrations,
} = useLaunchStore(
useShallow((state) => ({
hide: state.hide,
date: state.date,
setDate: state.setDate,
repeater: state.repeater,
setRepeater: state.setRepeater,
tags: state.tags,
setTags: state.setTags,
selectedIntegrations: state.selectedIntegrations,
integrations: state.integrations,
setSelectedIntegrations: state.setSelectedIntegrations,
}))
);
const deletePost = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
setLoading(false);
return;
}
await fetch(`/posts/${existingData.group}`, {
method: 'DELETE',
});
mutate();
modal.closeAll();
return;
}, [existingData, mutate, modal]);
const askClose = useCallback(async () => {
if (
await deleteDialog(
t(
'are_you_sure_you_want_to_close_this_modal_all_data_will_be_lost',
'Are you sure you want to close this modal? (all data will be lost)'
),
t('yes_close_it', 'Yes, close it!')
)
) {
if (customClose) {
customClose();
return;
}
modal.closeAll();
}
}, []);
const changeCustomer = useCallback(
(customer: string) => {
const neededIntegrations = integrations.filter(
(p) => p?.customer?.id === customer
);
setSelectedIntegrations(
neededIntegrations.map((p) => ({
settings: {},
selectedIntegrations: p,
}))
);
},
[integrations]
);
useKeypress('Escape', askClose);
const schedule = useCallback(
(type: 'draft' | 'now' | 'schedule') => async () => {
const checkAllValid = await ref.current.checkAllValid();
if (type !== 'draft') {
const notEnoughChars = checkAllValid.filter((p: any) => {
return p.values.some((a: any) => {
return (
countCharacters(a.content, p?.integration?.identifier || '') < 6
);
});
});
for (const item of notEnoughChars) {
toaster.show(
'' +
item.integration.name +
' post is too short, it must be at least 6 characters',
'warning'
);
item.preview();
return;
}
for (const item of checkAllValid) {
if (item.valid === false) {
toaster.show('Some fields are not valid', 'warning');
item.fix();
return;
}
if (item.errors !== true) {
toaster.show(
`${capitalize(item.integration.identifier.split('-')[0])} (${
item.integration.name
}): ${item.errors}`,
'warning'
);
item.preview();
return;
}
}
const sliceNeeded = checkAllValid.filter((p: any) => {
return p.values.some((a: any) => {
return (
countCharacters(a.content, p?.integration?.identifier || '') >
(p.maximumCharacters || 1000000)
);
});
});
for (const item of sliceNeeded) {
if (
!(await deleteDialog(
`${item?.integration?.name} (${item?.integration?.identifier}) post is too long, it will be cropped, do you want to continue?`,
'Yes, continue'
))
) {
item.preview();
return;
}
}
}
const shortLinkUrl = await (
await fetch('/posts/should-shortlink', {
method: 'POST',
body: JSON.stringify({
messages: checkAllValid.flatMap((p: any) =>
p.values.flatMap((a: any) => a.content)
),
}),
})
).json();
const shortLink = !shortLinkUrl.ask
? false
: await deleteDialog(
'Do you want to shortlink the URLs? it will let you get statistics over clicks',
'Yes, shortlink it!'
);
const group = existingData.group || makeId(10);
const data = {
type,
inter: repeater || undefined,
tags,
shortLink,
date: date.utc().format('YYYY-MM-DDTHH:mm:ss'),
posts: checkAllValid.map((p: any) => ({
integration: p.integration,
group,
settings: p.settings,
value: p.values.map((a: any) => ({
...a,
image: a.media || [],
content: a.content.slice(0, p.maximumCharacters || 1000000),
})),
})),
};
addEditSets
? addEditSets(data)
: await fetch('/posts', {
method: 'POST',
body: JSON.stringify(data),
});
if (!addEditSets) {
mutate();
toaster.show(
!existingData.integration
? 'Added successfully'
: 'Updated successfully'
);
}
if (customClose) {
setTimeout(() => {
customClose();
}, 2000);
}
if (!addEditSets) {
modal.closeAll();
}
},
[ref, repeater, tags, date, addEditSets]
);
return (
<>
<div
className={clsx(
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
)}
>
<div
className={clsx(
'flex flex-1 flex-col gap-[16px] transition-all duration-700 whitespace-nowrap'
)}
>
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0">
<TopTitle
title={
existingData.integration
? t('update_post', 'Update Existing Post')
: t('create_new_post', 'Create Post')
}
>
<div className="flex items-center">
<RepeatComponent repeat={repeater} onChange={setRepeater} />
<DatePicker onChange={setDate} date={date} />
</div>
</TopTitle>
<PicksSocialsComponent toolTip={true} />
<div>
{!existingData.integration && <SelectCurrent />}
<div className="flex gap-[4px]">
<div className="flex-1 editor text-textColor gap-[10px] flex-col flex">
{!hide && <EditorWrapper totalPosts={1} value="" />}
</div>
</div>
</div>
</div>
<div className="relative min-h-[68px] flex flex-col rounded-[4px] border border-customColor6 bg-sixth">
<div className="gap-[10px] relative flex flex-col justify-center items-center min-h-full pe-[16px]">
<div
id="add-edit-post-dialog-buttons"
className="flex flex-row flex-wrap w-full h-full gap-[10px] justify-end items-center"
>
<div className="flex justify-center items-center gap-[5px] h-full">
{!!existingData.integration && (
<Button
onClick={deletePost}
className="rounded-[4px] border-2 border-red-400 text-red-400"
secondary={true}
>
{t('delete_post', 'Delete Post')}
</Button>
)}
{!addEditSets && (
<Button
onClick={schedule('draft')}
className="rounded-[4px] border-2 border-customColor21"
secondary={true}
disabled={selectedIntegrations.length === 0}
>
{t('save_as_draft', 'Save as draft')}
</Button>
)}
{addEditSets && (
<Button
className="rounded-[4px] relative group"
disabled={selectedIntegrations.length === 0 || loading}
onClick={schedule('draft')}
>
Save Set
</Button>
)}
{!addEditSets && (
<Button
className="rounded-[4px] relative group"
disabled={selectedIntegrations.length === 0 || loading}
onClick={schedule('schedule')}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center text-white">
{selectedIntegrations.length === 0
? t(
'select_channels_from_circles',
'Select channels from the circles above'
)
: !existingData?.integration
? t('add_to_calendar', 'Add to calendar')
: t('update', 'Update')}
</div>
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.0233 7.14804L9.39828 12.773C9.34604 12.8253 9.284 12.8668 9.21572 12.8951C9.14743 12.9234 9.07423 12.938 9.00031 12.938C8.92639 12.938 8.8532 12.9234 8.78491 12.8951C8.71662 12.8668 8.65458 12.8253 8.60234 12.773L2.97734 7.14804C2.8718 7.04249 2.8125 6.89934 2.8125 6.75007C2.8125 6.6008 2.8718 6.45765 2.97734 6.3521C3.08289 6.24655 3.22605 6.18726 3.37531 6.18726C3.52458 6.18726 3.66773 6.24655 3.77328 6.3521L9.00031 11.5798L14.2273 6.3521C14.2796 6.29984 14.3417 6.25838 14.4099 6.2301C14.4782 6.20181 14.5514 6.18726 14.6253 6.18726C14.6992 6.18726 14.7724 6.20181 14.8407 6.2301C14.909 6.25838 14.971 6.29984 15.0233 6.3521C15.0755 6.40436 15.117 6.46641 15.1453 6.53469C15.1736 6.60297 15.1881 6.67616 15.1881 6.75007C15.1881 6.82398 15.1736 6.89716 15.1453 6.96545C15.117 7.03373 15.0755 7.09578 15.0233 7.14804Z"
fill="white"
/>
</svg>
<div
onClick={schedule('now')}
className={clsx(
'hidden group-hover:flex hover:flex flex-col justify-center absolute start-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
loading &&
'cursor-not-allowed pointer-events-none opacity-50'
)}
>
{t('post_now', 'Post now')}
</div>
</div>
</div>
</Button>
)}
</div>
</div>
</div>
</div>
</div>
<div
className={clsx(
'flex-grow w-[650px] max-w-[650px] min-w-[650px] flex gap-[20px] flex-col rounded-[4px] border-customColor6 bg-sixth flex-1 transition-all duration-700'
)}
>
<div className="mx-[16px]">
<TopTitle title="" removeTitle={true}>
<div className="flex flex-1 gap-[10px]">
<div>
<TagsComponent
name="tags"
label={t('tags', 'Tags')}
initial={tags}
onChange={(e) => setTags(e.target.value)}
/>
</div>
<SelectCustomer
onChange={changeCustomer}
integrations={integrations}
/>
</div>
<svg
onClick={askClose}
width="10"
height="11"
viewBox="0 0 10 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="cursor-pointer"
>
<path
d="M9.85403 9.64628C9.90048 9.69274 9.93733 9.74789 9.96247 9.80859C9.98762 9.86928 10.0006 9.93434 10.0006 10C10.0006 10.0657 9.98762 10.1308 9.96247 10.1915C9.93733 10.2522 9.90048 10.3073 9.85403 10.3538C9.80757 10.4002 9.75242 10.4371 9.69173 10.4622C9.63103 10.4874 9.56598 10.5003 9.50028 10.5003C9.43458 10.5003 9.36953 10.4874 9.30883 10.4622C9.24813 10.4371 9.19298 10.4002 9.14653 10.3538L5.00028 6.20691L0.854028 10.3538C0.760208 10.4476 0.63296 10.5003 0.500278 10.5003C0.367596 10.5003 0.240348 10.4476 0.146528 10.3538C0.0527077 10.26 2.61548e-09 10.1327 0 10C-2.61548e-09 9.86735 0.0527077 9.7401 0.146528 9.64628L4.2934 5.50003L0.146528 1.35378C0.0527077 1.25996 0 1.13272 0 1.00003C0 0.867352 0.0527077 0.740104 0.146528 0.646284C0.240348 0.552464 0.367596 0.499756 0.500278 0.499756C0.63296 0.499756 0.760208 0.552464 0.854028 0.646284L5.00028 4.79316L9.14653 0.646284C9.24035 0.552464 9.3676 0.499756 9.50028 0.499756C9.63296 0.499756 9.76021 0.552464 9.85403 0.646284C9.94785 0.740104 10.0006 0.867352 10.0006 1.00003C10.0006 1.13272 9.94785 1.25996 9.85403 1.35378L5.70715 5.50003L9.85403 9.64628Z"
fill="currentColor"
/>
</svg>
</TopTitle>
</div>
<div className="flex-1 flex flex-col p-[16px] pt-0">
<ShowAllProviders ref={ref} />
</div>
</div>
</div>
<CopilotPopup
hitEscapeToClose={false}
clickOutsideToClose={true}
instructions={`
You are an assistant that help the user to schedule their social media posts,
Here are the things you can do:
- Add a new comment / post to the list of posts
- Delete a comment / post from the list of posts
- Add content to the comment / post
- Activate or deactivate the comment / post
Post content can be added using the addPostContentFor{num} function.
After using the addPostFor{num} it will create a new addPostContentFor{num+ 1} function.
`}
labels={{
title: 'Your Assistant',
initial: 'Hi! 👋 How can I assist you today?',
}}
/>
</>
);
};

View File

@ -0,0 +1,87 @@
'use client';
import { FC } from 'react';
import clsx from 'clsx';
import Image from 'next/image';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
export const PicksSocialsComponent: FC<{ toolTip?: boolean }> = ({
toolTip,
}) => {
const exising = useExistingData();
const { locked, addOrRemoveSelectedIntegration, integrations, selectedIntegrations } =
useLaunchStore(
useShallow((state) => ({
integrations: state.integrations,
selectedIntegrations: state.selectedIntegrations,
addOrRemoveSelectedIntegration: state.addOrRemoveSelectedIntegration,
locked: state.locked,
}))
);
return (
<div className={clsx('flex', locked && 'opacity-50 pointer-events-none')}>
<div className="flex">
<div className="innerComponent">
<div className="grid grid-cols-13 gap-[10px]">
{integrations
.filter((f) => !f.inBetweenSteps)
.map((integration) => (
<div
key={integration.id}
className="flex gap-[8px] items-center"
{...(toolTip && {
'data-tooltip-id': 'tooltip',
'data-tooltip-content': integration.name,
})}
>
<div
onClick={() => {
if (exising.integration) {
return;
}
addOrRemoveSelectedIntegration(integration, {});
}}
className={clsx(
'cursor-pointer relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
selectedIntegrations.findIndex(
(p) => p.integration.id === integration.id
) === -1
? 'opacity-40'
: ''
)}
>
<Image
src={integration.picture || '/no-picture.jpg'}
className="rounded-full"
alt={integration.identifier}
width={32}
height={32}
/>
{integration.identifier === 'youtube' ? (
<img
src="/icons/platforms/youtube.svg"
className="absolute z-10 -bottom-[5px] -end-[5px]"
width={20}
/>
) : (
<Image
src={`/icons/platforms/${integration.identifier}.png`}
className="rounded-full absolute z-10 -bottom-[5px] -end-[5px] border border-fifth"
alt={integration.identifier}
width={20}
height={20}
/>
)}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};

View File

@ -1,5 +1,7 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/new-launch/finisher/thread.finisher';
const SettingsComponent = () => {
return <ThreadFinisher />;

View File

@ -1,11 +1,13 @@
'use client';
import { FC, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import useSWR from 'swr';
import clsx from 'clsx';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const FacebookContinue: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -1,11 +1,13 @@
'use client';
import { FC, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import useSWR from 'swr';
import clsx from 'clsx';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const InstagramContinue: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -1,11 +1,13 @@
'use client';
import { FC, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import useSWR from 'swr';
import clsx from 'clsx';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const LinkedinContinue: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -0,0 +1,10 @@
'use client';
import { InstagramContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/instagram/instagram.continue';
import { FacebookContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/facebook/facebook.continue';
import { LinkedinContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/linkedin/linkedin.continue';
export const continueProviderList = {
instagram: InstagramContinue,
facebook: FacebookContinue,
'linkedin-page': LinkedinContinue,
};

View File

@ -1,24 +1,19 @@
'use client';
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
import { SelectOrganization } from '@gitroom/frontend/components/launches/providers/devto/select.organization';
import { DevtoTags } from '@gitroom/frontend/components/launches/providers/devto/devto.tags';
import { SelectOrganization } from '@gitroom/frontend/components/new-launch/providers/devto/select.organization';
import { DevtoTags } from '@gitroom/frontend/components/new-launch/providers/devto/devto.tags';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import clsx from 'clsx';
import localFont from 'next/font/local';
import MDEditor from '@uiw/react-md-editor';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { Canonical } from '@gitroom/react/form/canonical';
const font = localFont({
src: [
{
path: './fonts/SFNS.woff2',
},
],
});
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const DevtoPreview: FC = () => {
const { value } = useIntegration();
const settings = useSettings();
@ -31,7 +26,6 @@ const DevtoPreview: FC = () => {
return (
<div
className={clsx(
font.className,
'font-[800] flex h-[1000px] w-[699.8px] rounded-[10px] bg-customColor32 overflow-hidden overflow-y-auto flex-col gap-[32px]'
)}
>
@ -57,7 +51,6 @@ const DevtoPreview: FC = () => {
style={{
whiteSpace: 'pre-wrap',
}}
className={font.className}
skipHtml={true}
source={value.map((p) => p.content).join('\n')}
/>

View File

@ -1,8 +1,10 @@
'use client';
import { FC, useCallback, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';
import interClass from '@gitroom/react/helpers/inter.font';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const DevtoTags: FC<{
name: string;
label: string;

View File

@ -1,8 +1,10 @@
'use client';
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';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const SelectOrganization: FC<{
name: string;
onChange: (event: {

View File

@ -1,3 +1,5 @@
'use client';
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';

View File

@ -1,7 +1,9 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { FC } from 'react';
import { DiscordDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/discord.dto';
import { DiscordChannelSelect } from '@gitroom/frontend/components/launches/providers/discord/discord.channel.select';
import { DiscordChannelSelect } from '@gitroom/frontend/components/new-launch/providers/discord/discord.channel.select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const DiscordComponent: FC = () => {
const form = useSettings();

View File

@ -1,8 +1,10 @@
'use client';
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
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 { DribbbleTeams } from '@gitroom/frontend/components/new-launch/providers/dribbble/dribbble.teams';
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
const DribbbleSettings: FC = () => {
const { register, control } = useSettings();

View File

@ -1,3 +1,5 @@
'use client';
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';

View File

@ -0,0 +1,4 @@
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
export default withProvider(null, undefined, undefined, undefined, 63206);

View File

@ -1,20 +1,19 @@
'use client';
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { HashnodePublications } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.publications';
import { HashnodeTags } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.tags';
import { HashnodePublications } from '@gitroom/frontend/components/new-launch/providers/hashnode/hashnode.publications';
import { HashnodeTags } from '@gitroom/frontend/components/new-launch/providers/hashnode/hashnode.tags';
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import { Plus_Jakarta_Sans } from 'next/font/google';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
import { Canonical } from '@gitroom/react/form/canonical';
const font = Plus_Jakarta_Sans({
subsets: ['latin'],
});
const HashnodePreview: FC = () => {
const { value } = useIntegration();
const settings = useSettings();
@ -27,7 +26,6 @@ const HashnodePreview: FC = () => {
return (
<div
className={clsx(
font.className,
'text-center text-black flex h-[1000px] w-[699.8px] rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[32px]'
)}
>
@ -54,7 +52,6 @@ const HashnodePreview: FC = () => {
whiteSpace: 'pre-wrap',
color: 'black',
}}
className={font.className}
skipHtml={true}
source={value.map((p) => p.content).join('\n')}
/>

View File

@ -1,3 +1,5 @@
'use client';
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';

View File

@ -1,3 +1,5 @@
'use client';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';

View File

@ -0,0 +1,254 @@
'use client';
import React, {
FC,
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
} from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { IsOptional } from 'class-validator';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { Button } from '@gitroom/react/form/button';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { InternalChannels } from '@gitroom/frontend/components/launches/internal.channels';
import { capitalize } from 'lodash';
class Empty {
@IsOptional()
empty: string;
}
export const withProvider = function <T extends object>(
SettingsComponent: FC<{
values?: any;
}> | null,
CustomPreviewComponent?: FC<{
maximumCharacters?: number;
}>,
dto?: any,
checkValidity?: (
value: Array<
Array<{
path: string;
}>
>,
settings: T,
additionalSettings: any
) => Promise<string | true>,
maximumCharacters?: number | ((settings: any) => number)
) {
return forwardRef((props: { id: string }, ref) => {
const t = useT();
const fetch = useFetch();
const {
current,
integrations,
selectedIntegration,
setCurrent,
internal,
global,
date,
isGlobal,
tab,
setTab,
} = useLaunchStore(
useShallow((state) => ({
date: state.date,
tab: state.tab,
setTab: state.setTab,
global: state.global,
internal: state.internal.find((p) => p.integration.id === props.id),
integrations: state.selectedIntegrations,
current: state.current === props.id,
isGlobal: state.current === 'global',
setCurrent: state.setCurrent,
selectedIntegration: state.selectedIntegrations.find(
(p) => p.integration.id === props.id
),
}))
);
const getInternalPlugs = useCallback(async () => {
return (
await fetch(
`/integrations/${selectedIntegration.integration.identifier}/internal-plugs`
)
).json();
}, [selectedIntegration.integration.identifier]);
const { data, isLoading } = useSWR(
`internal-${selectedIntegration.integration.identifier}`,
getInternalPlugs,
{
revalidateOnReconnect: true,
}
);
const value = useMemo(() => {
if (internal?.integrationValue?.length) {
return internal.integrationValue;
}
return global;
}, [internal, global, isGlobal]);
const form = useForm({
resolver: classValidatorResolver(dto || Empty),
...(Object.keys(selectedIntegration.settings).length > 0
? { values: { ...selectedIntegration.settings } }
: {}),
mode: 'all',
criteriaMode: 'all',
reValidateMode: 'onChange',
});
useImperativeHandle(
ref,
() => ({
isValid: async () => {
const settings = form.getValues();
return {
id: props.id,
identifier: selectedIntegration.integration.identifier,
integration: selectedIntegration.integration,
valid: await form.trigger(),
errors: checkValidity
? await checkValidity(
value.map((p) => p.media || []),
settings,
JSON.parse(
selectedIntegration.integration.additionalSettings || '[]'
)
)
: true,
settings,
values: value,
maximumCharacters:
typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
selectedIntegration.integration.additionalSettings || '[]'
)
),
fix: () => {
setTab(1);
setCurrent(props.id);
},
preview: () => {
setTab(0);
setCurrent(props.id);
},
};
},
getValues: () => {
return {
id: props.id,
identifier: selectedIntegration.integration.identifier,
values: value,
settings: form.getValues(),
};
},
trigger: () => {
return form.trigger();
},
}),
[value]
);
return (
<IntegrationContext.Provider
value={{
date,
integration: selectedIntegration.integration,
allIntegrations: integrations.map((p) => p.integration),
value: value.map((p) => ({
id: p.id,
content: p.content,
image: p.media,
})),
}}
>
<FormProvider {...form}>
<div className={current ? '' : 'hidden'}>
<div className="flex gap-[4px] mb-[20px]">
<div className="flex-1 flex">
<Button
onClick={() => setTab(0)}
secondary={tab !== 0 && !!SettingsComponent}
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
>
{t('preview', 'Preview')}
</Button>
</div>
{!!SettingsComponent && (
<div className="flex-1 flex">
<Button
onClick={() => setTab(1)}
secondary={tab !== 1}
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
>
{t('settings', 'Settings')} (
{capitalize(
selectedIntegration.integration.identifier.split('-')[0]
)}
)
</Button>
</div>
)}
</div>
{(tab === 0 || !SettingsComponent) &&
!value?.[0]?.content?.length && (
<div>{t('start_writing_your_post', 'Start writing your post for a preview')}</div>
)}
{(tab === 0 || !SettingsComponent) &&
!!value?.[0]?.content?.length &&
(CustomPreviewComponent ? (
<CustomPreviewComponent
maximumCharacters={
typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
selectedIntegration.integration
.additionalSettings || '[]'
)
)
}
/>
) : (
<GeneralPreviewComponent
maximumCharacters={
typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
selectedIntegration.integration
.additionalSettings || '[]'
)
)
}
/>
))}
{SettingsComponent && (
<div className={tab === 1 ? '' : 'hidden'}>
<SettingsComponent />
{!!data?.internalPlugs?.length && (
<InternalChannels plugs={data?.internalPlugs} />
)}
</div>
)}
</div>
</FormProvider>
</IntegrationContext.Provider>
);
});
};

View File

@ -1,9 +1,11 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { FC } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/launches/providers/instagram/instagram.tags';
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/new-launch/providers/instagram/instagram.tags';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
const postType = [
{

View File

@ -1,3 +1,5 @@
'use client';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';

View File

@ -1,5 +1,7 @@
'use client';
import { FC, useCallback } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useFieldArray } from 'react-hook-form';
import { Button } from '@gitroom/react/form/button';

View File

@ -1,3 +1,5 @@
'use client';
import { FC, FormEvent, useCallback, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Input } from '@gitroom/react/form/input';

View File

@ -1,4 +1,6 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { Checkbox } from '@gitroom/react/form/checkbox';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';

View File

@ -0,0 +1,4 @@
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
export default withProvider(null, undefined, undefined, undefined, 500);

View File

@ -1,40 +1,18 @@
'use client';
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediumPublications } from '@gitroom/frontend/components/launches/providers/medium/medium.publications';
import { MediumTags } from '@gitroom/frontend/components/launches/providers/medium/medium.tags';
import { MediumPublications } from '@gitroom/frontend/components/new-launch/providers/medium/medium.publications';
import { MediumTags } from '@gitroom/frontend/components/new-launch/providers/medium/medium.tags';
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import localFont from 'next/font/local';
import { Canonical } from '@gitroom/react/form/canonical';
import interClass from '@gitroom/react/helpers/inter.font';
const charter = localFont({
src: [
{
path: './fonts/Charter Regular.ttf',
weight: 'normal',
style: 'normal',
},
{
path: './fonts/Charter Italic.ttf',
weight: 'normal',
style: 'italic',
},
{
path: './fonts/Charter Bold.ttf',
weight: '700',
style: 'normal',
},
{
path: './fonts/Charter Bold Italic.ttf',
weight: '700',
style: 'italic',
},
],
});
const MediumPreview: FC = () => {
const { value } = useIntegration();
const settings = useSettings();
@ -57,7 +35,6 @@ const MediumPreview: FC = () => {
whiteSpace: 'pre-wrap',
color: '#242424',
}}
className={charter.className}
skipHtml={true}
source={value.map((p) => p.content).join('\n')}
/>

View File

@ -1,3 +1,5 @@
'use client';
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';

View File

@ -1,3 +1,5 @@
'use client';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';

View File

@ -0,0 +1,12 @@
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async () => {
return true;
},
undefined
);

View File

@ -1,3 +1,5 @@
'use client';
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';

View File

@ -1,7 +1,9 @@
'use client';
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { PinterestBoard } from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.board';
import { PinterestBoard } from '@gitroom/frontend/components/new-launch/providers/pinterest/pinterest.board';
import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/pinterest.dto';
import { Input } from '@gitroom/react/form/input';
import { ColorPicker } from '@gitroom/react/form/color.picker';

View File

@ -1,8 +1,9 @@
'use client';
import { FC, useCallback } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/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 { Subreddit } from '@gitroom/frontend/components/launches/providers/reddit/subreddit';
import { Subreddit } from '@gitroom/frontend/components/new-launch/providers/reddit/subreddit';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useFieldArray, useWatch } from 'react-hook-form';
import { Button } from '@gitroom/react/form/button';
@ -17,6 +18,7 @@ import MDEditor from '@uiw/react-md-editor';
import interClass from '@gitroom/react/helpers/inter.font';
import Image from 'next/image';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
const RenderRedditComponent: FC<{
type: string;
images?: Array<{

View File

@ -1,3 +1,5 @@
'use client';
import { FC, FormEvent, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Input } from '@gitroom/react/form/input';

View File

@ -0,0 +1,215 @@
'use client';
import DevtoProvider from '@gitroom/frontend/components/new-launch/providers/devto/devto.provider';
import XProvider from '@gitroom/frontend/components/new-launch/providers/x/x.provider';
import LinkedinProvider from '@gitroom/frontend/components/new-launch/providers/linkedin/linkedin.provider';
import RedditProvider from '@gitroom/frontend/components/new-launch/providers/reddit/reddit.provider';
import MediumProvider from '@gitroom/frontend/components/new-launch/providers/medium/medium.provider';
import HashnodeProvider from '@gitroom/frontend/components/new-launch/providers/hashnode/hashnode.provider';
import FacebookProvider from '@gitroom/frontend/components/new-launch/providers/facebook/facebook.provider';
import InstagramProvider from '@gitroom/frontend/components/new-launch/providers/instagram/instagram.collaborators';
import YoutubeProvider from '@gitroom/frontend/components/new-launch/providers/youtube/youtube.provider';
import TiktokProvider from '@gitroom/frontend/components/new-launch/providers/tiktok/tiktok.provider';
import PinterestProvider from '@gitroom/frontend/components/new-launch/providers/pinterest/pinterest.provider';
import DribbbleProvider from '@gitroom/frontend/components/new-launch/providers/dribbble/dribbble.provider';
import ThreadsProvider from '@gitroom/frontend/components/new-launch/providers/threads/threads.provider';
import DiscordProvider from '@gitroom/frontend/components/new-launch/providers/discord/discord.provider';
import SlackProvider from '@gitroom/frontend/components/new-launch/providers/slack/slack.provider';
import MastodonProvider from '@gitroom/frontend/components/new-launch/providers/mastodon/mastodon.provider';
import BlueskyProvider from '@gitroom/frontend/components/new-launch/providers/bluesky/bluesky.provider';
import LemmyProvider from '@gitroom/frontend/components/new-launch/providers/lemmy/lemmy.provider';
import WarpcastProvider from '@gitroom/frontend/components/new-launch/providers/warpcast/warpcast.provider';
import TelegramProvider from '@gitroom/frontend/components/new-launch/providers/telegram/telegram.provider';
import NostrProvider from '@gitroom/frontend/components/new-launch/providers/nostr/nostr.provider';
import VkProvider from '@gitroom/frontend/components/new-launch/providers/vk/vk.provider';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
import React, { createRef, FC, forwardRef, useImperativeHandle } from 'react';
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { Button } from '@gitroom/react/form/button';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Providers = [
{
identifier: 'devto',
component: DevtoProvider,
},
{
identifier: 'x',
component: XProvider,
},
{
identifier: 'linkedin',
component: LinkedinProvider,
},
{
identifier: 'linkedin-page',
component: LinkedinProvider,
},
{
identifier: 'reddit',
component: RedditProvider,
},
{
identifier: 'medium',
component: MediumProvider,
},
{
identifier: 'hashnode',
component: HashnodeProvider,
},
{
identifier: 'facebook',
component: FacebookProvider,
},
{
identifier: 'instagram',
component: InstagramProvider,
},
{
identifier: 'instagram-standalone',
component: InstagramProvider,
},
{
identifier: 'youtube',
component: YoutubeProvider,
},
{
identifier: 'tiktok',
component: TiktokProvider,
},
{
identifier: 'pinterest',
component: PinterestProvider,
},
{
identifier: 'dribbble',
component: DribbbleProvider,
},
{
identifier: 'threads',
component: ThreadsProvider,
},
{
identifier: 'discord',
component: DiscordProvider,
},
{
identifier: 'slack',
component: SlackProvider,
},
{
identifier: 'mastodon',
component: MastodonProvider,
},
{
identifier: 'bluesky',
component: BlueskyProvider,
},
{
identifier: 'lemmy',
component: LemmyProvider,
},
{
identifier: 'wrapcast',
component: WarpcastProvider,
},
{
identifier: 'telegram',
component: TelegramProvider,
},
{
identifier: 'nostr',
component: NostrProvider,
},
{
identifier: 'vk',
component: VkProvider,
},
];
export const ShowAllProviders = forwardRef((props, ref) => {
const { date, current, global, selectedIntegrations, allIntegrations } =
useLaunchStore(
useShallow((state) => ({
date: state.date,
selectedIntegrations: state.selectedIntegrations,
allIntegrations: state.integrations,
current: state.current,
global: state.global,
}))
);
const t = useT();
useImperativeHandle(ref, () => ({
checkAllValid: async () => {
return Promise.all(
selectedIntegrations.map(async (p) => await p.ref?.current.isValid())
);
},
getAllValues: async () => {
return Promise.all(
selectedIntegrations.map(async (p) => await p.ref?.current.getValues())
);
},
triggerAll: () => {
return selectedIntegrations.map(async (p) => await p.ref?.current.trigger());
}
}));
return (
<div className="w-full flex flex-col flex-1">
{current === 'global' && (
<IntegrationContext.Provider
value={{
date,
integration:
selectedIntegrations?.[0]?.integration || allIntegrations?.[0],
allIntegrations: selectedIntegrations.map((p) => p.integration),
value: global.map((p) => ({
id: p.id,
content: p.content,
image: p.media,
})),
}}
>
<div className="flex gap-[4px] mb-[20px]">
<div className="flex-1 flex">
<Button
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
>
{t('preview', 'Preview')}
</Button>
</div>
</div>
{global?.[0]?.content?.length === 0 ? (
<div>{t('start_writing_your_post', 'Start writing your post for a preview')}</div>
) : (
<GeneralPreviewComponent maximumCharacters={100000000} />
)}
</IntegrationContext.Provider>
)}
{selectedIntegrations.map((integration) => {
const { component: ProviderComponent } = Providers.find(
(provider) =>
provider.identifier === integration.integration.identifier
) || {
component: Empty,
};
return (
<ProviderComponent
ref={integration.ref}
key={integration.integration.id}
id={integration.integration.id}
/>
);
})}
</div>
);
});
export const Empty: FC = () => {
return null;
};

View File

@ -1,3 +1,5 @@
'use client';
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';

View File

@ -1,7 +1,9 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { FC } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { SlackChannelSelect } from '@gitroom/frontend/components/launches/providers/slack/slack.channel.select';
import { SlackChannelSelect } from '@gitroom/frontend/components/new-launch/providers/slack/slack.channel.select';
import { SlackDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/slack.dto';
const SlackComponent: FC = () => {
const form = useSettings();

View File

@ -0,0 +1,12 @@
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async () => {
return true;
},
4096
);

View File

@ -0,0 +1,17 @@
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/new-launch/finisher/thread.finisher';
const SettingsComponent = () => {
return <ThreadFinisher />;
};
export default withProvider(
SettingsComponent,
undefined,
undefined,
async () => {
return true;
},
500
);

View File

@ -1,3 +1,5 @@
'use client';
import {
FC,
ReactEventHandler,
@ -6,7 +8,7 @@ import {
useMemo,
useState,
} from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Select } from '@gitroom/react/form/select';

View File

@ -0,0 +1,12 @@
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async (posts) => {
return true;
},
2048
);

View File

@ -1,3 +1,5 @@
'use client';
import { FC, FormEvent, useCallback, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Input } from '@gitroom/react/form/input';

View File

@ -1,4 +1,6 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { FC, useCallback } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useFieldArray } from 'react-hook-form';

View File

@ -1,5 +1,7 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher';
'use client';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/new-launch/finisher/thread.finisher';
import { Select } from '@gitroom/react/form/select';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';

View File

@ -1,9 +1,11 @@
'use client';
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediumTags } from '@gitroom/frontend/components/launches/providers/medium/medium.tags';
import { MediumTags } from '@gitroom/frontend/components/new-launch/providers/medium/medium.tags';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
import { Select } from '@gitroom/react/form/select';
const type = [

View File

@ -0,0 +1,150 @@
'use client';
import {
FC,
RefObject,
useEffect,
useRef,
useState,
} from 'react';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import clsx from 'clsx';
import Image from 'next/image';
import { useShallow } from 'zustand/react/shallow';
export function useHasScroll(ref: RefObject<HTMLElement>): boolean {
const [hasHorizontalScroll, setHasHorizontalScroll] = useState(false);
useEffect(() => {
if (!ref.current) return;
const checkScroll = () => {
const el = ref.current;
if (el) {
setHasHorizontalScroll(el.scrollWidth > el.clientWidth);
}
};
checkScroll(); // initial check
const resizeObserver = new ResizeObserver(checkScroll);
resizeObserver.observe(ref.current);
const mutationObserver = new MutationObserver(checkScroll);
mutationObserver.observe(ref.current, {
childList: true,
subtree: true,
characterData: true,
});
return () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, [ref]);
return hasHorizontalScroll;
}
export const SelectCurrent: FC = () => {
const { selectedIntegrations, current, setCurrent, locked, setHide, hide } =
useLaunchStore(
useShallow((state) => ({
selectedIntegrations: state.selectedIntegrations,
current: state.current,
setCurrent: state.setCurrent,
locked: state.locked,
hide: state.hide,
setHide: state.setHide,
}))
);
const contentRef = useRef<HTMLDivElement>(null);
const hasScroll = useHasScroll(contentRef);
useEffect(() => {
if (!hide) {
return;
}
setHide(false);
}, [hide]);
return (
<>
<div className="left-0 absolute w-full z-[100] px-[16px]">
<div
ref={contentRef}
className={clsx(
'flex gap-[3px] w-full overflow-x-auto scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
locked && 'opacity-50 pointer-events-none'
)}
>
<div
onClick={() => {
setHide(true);
setCurrent('global');
}}
className="cursor-pointer flex gap-[8px] items-center bg-customColor2 p-[10px] rounded-tl-[4px] rounded-tr-[4px]"
>
<div className={clsx(current !== 'global' ? 'opacity-40' : '')}>
<svg
width="20"
height="20"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 3C13.4288 3 10.9154 3.76244 8.77759 5.1909C6.63975 6.61935 4.97351 8.64968 3.98957 11.0251C3.00563 13.4006 2.74819 16.0144 3.2498 18.5362C3.75141 21.0579 4.98953 23.3743 6.80762 25.1924C8.6257 27.0105 10.9421 28.2486 13.4638 28.7502C15.9856 29.2518 18.5995 28.9944 20.9749 28.0104C23.3503 27.0265 25.3807 25.3603 26.8091 23.2224C28.2376 21.0846 29 18.5712 29 16C28.9964 12.5533 27.6256 9.24882 25.1884 6.81163C22.7512 4.37445 19.4467 3.00364 16 3ZM12.7038 21H19.2963C18.625 23.2925 17.5 25.3587 16 26.9862C14.5 25.3587 13.375 23.2925 12.7038 21ZM12.25 19C11.9183 17.0138 11.9183 14.9862 12.25 13H19.75C20.0817 14.9862 20.0817 17.0138 19.75 19H12.25ZM5.00001 16C4.99914 14.9855 5.13923 13.9759 5.41626 13H10.2238C9.92542 14.9889 9.92542 17.0111 10.2238 19H5.41626C5.13923 18.0241 4.99914 17.0145 5.00001 16ZM19.2963 11H12.7038C13.375 8.7075 14.5 6.64125 16 5.01375C17.5 6.64125 18.625 8.7075 19.2963 11ZM21.7763 13H26.5838C27.1388 14.9615 27.1388 17.0385 26.5838 19H21.7763C22.0746 17.0111 22.0746 14.9889 21.7763 13ZM25.7963 11H21.3675C20.8572 8.99189 20.0001 7.0883 18.835 5.375C20.3236 5.77503 21.7119 6.48215 22.9108 7.45091C24.1097 8.41967 25.0926 9.62861 25.7963 11ZM13.165 5.375C11.9999 7.0883 11.1428 8.99189 10.6325 11H6.20376C6.90741 9.62861 7.89029 8.41967 9.08918 7.45091C10.2881 6.48215 11.6764 5.77503 13.165 5.375ZM6.20376 21H10.6325C11.1428 23.0081 11.9999 24.9117 13.165 26.625C11.6764 26.225 10.2881 25.5178 9.08918 24.5491C7.89029 23.5803 6.90741 22.3714 6.20376 21ZM18.835 26.625C20.0001 24.9117 20.8572 23.0081 21.3675 21H25.7963C25.0926 22.3714 24.1097 23.5803 22.9108 24.5491C21.7119 25.5178 20.3236 26.225 18.835 26.625Z"
fill="currentColor"
/>
</svg>
</div>
</div>
{selectedIntegrations.map(({ integration }) => (
<div
onClick={() => {
setHide(true);
setCurrent(integration.id);
}}
key={integration.id}
className="cursor-pointer flex gap-[8px] items-center bg-customColor2 p-[10px] rounded-tl-[4px] rounded-tr-[4px]"
>
<div
className={clsx(
'relative w-[20px] h-[20px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
current !== integration.id ? 'opacity-40' : ''
)}
>
<Image
src={integration.picture || '/no-picture.jpg'}
className="rounded-full"
alt={integration.identifier}
width={20}
height={20}
/>
{integration.identifier === 'youtube' ? (
<img
src="/icons/platforms/youtube.svg"
className="absolute z-10 -bottom-[5px] -end-[5px]"
width={20}
/>
) : (
<Image
src={`/icons/platforms/${integration.identifier}.png`}
className="rounded-full absolute z-10 -bottom-[5px] -end-[5px] border border-fifth"
alt={integration.identifier}
width={15}
height={15}
/>
)}
</div>
</div>
))}
</div>
</div>
<div className={clsx(hasScroll ? 'h-[55px]' : 'h-[40px]')} />
</>
);
};

View File

@ -0,0 +1,448 @@
'use client';
import { create } from 'zustand';
import dayjs from 'dayjs';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import { createRef, RefObject } from 'react';
import { arrayMoveImmutable } from 'array-move';
interface Values {
id: string;
content: string;
media: { id: string; path: string }[];
}
interface Internal {
integration: Integrations;
integrationValue: Values[];
}
interface SelectedIntegrations {
settings: any;
integration: Integrations;
ref?: RefObject<any>;
}
interface StoreState {
date: dayjs.Dayjs;
repeater?: number;
isCreateSet: boolean;
tags: { label: string; value: string }[];
tab: 0 | 1;
current: string;
locked: boolean;
hide: boolean;
setLocked: (locked: boolean) => void;
integrations: Integrations[];
selectedIntegrations: SelectedIntegrations[];
global: Values[];
internal: Internal[];
addGlobalValue: (index: number, value: Values[]) => void;
addInternalValue: (
index: number,
integrationId: string,
value: Values[]
) => void;
setGlobalValue: (value: Values[]) => void;
setInternalValue: (integrationId: string, value: Values[]) => void;
deleteGlobalValue: (index: number) => void;
deleteInternalValue: (integrationId: string, index: number) => void;
addRemoveInternal: (integrationId: string) => void;
changeOrderGlobal: (index: number, direction: 'up' | 'down') => void;
changeOrderInternal: (
integrationId: string,
index: number,
direction: 'up' | 'down'
) => void;
setGlobalValueText: (index: number, content: string) => void;
setGlobalValueMedia: (
index: number,
media: { id: string; path: string }[]
) => void;
setInternalValueMedia: (
integrationId: string,
index: number,
media: { id: string; path: string }[]
) => void;
addGlobalValueMedia: (
index: number,
media: { id: string; path: string }[]
) => void;
removeGlobalValueMedia: (index: number, mediaIndex: number) => void;
setInternalValueText: (
integrationId: string,
index: number,
content: string
) => void;
addInternalValueMedia: (
integrationId: string,
index: number,
media: { id: string; path: string }[]
) => void;
removeInternalValueMedia: (
integrationId: string,
index: number,
mediaIndex: number
) => void;
setAllIntegrations: (integrations: Integrations[]) => void;
setCurrent: (current: string) => void;
addOrRemoveSelectedIntegration: (
integration: Integrations,
settings: any
) => void;
reset: () => void;
setSelectedIntegrations: (
params: { selectedIntegrations: Integrations; settings: any }[]
) => void;
setTab: (tab: 0 | 1) => void;
setHide: (hide: boolean) => void;
setDate: (date: dayjs.Dayjs) => void;
setRepeater: (repeater: number) => void;
setTags: (tags: { label: string; value: string }[]) => void;
setIsCreateSet: (isCreateSet: boolean) => void;
}
const initialState = {
date: dayjs(),
tags: [] as { label: string; value: string }[],
tab: 0 as 0,
isCreateSet: false,
current: 'global',
locked: false,
hide: false,
integrations: [] as Integrations[],
selectedIntegrations: [] as SelectedIntegrations[],
global: [] as Values[],
internal: [] as Internal[],
};
export const useLaunchStore = create<StoreState>()((set) => ({
...initialState,
setCurrent: (current: string) =>
set((state) => ({
current: current,
})),
addOrRemoveSelectedIntegration: (
integration: Integrations,
settings: any
) => {
set((state) => {
const existingIndex = state.selectedIntegrations.findIndex(
(i) => i.integration.id === integration.id
);
if (existingIndex > -1) {
return {
selectedIntegrations: state.selectedIntegrations.filter(
(_, index) => index !== existingIndex
),
};
}
return {
selectedIntegrations: [
...state.selectedIntegrations,
{ integration, settings, ref: createRef() },
],
};
});
},
addGlobalValue: (index: number, value: Values[]) =>
set((state) => {
if (!state.global.length) {
return { global: value };
}
return {
global: state.global.reduce((acc, item, i) => {
acc.push(item);
if (i === index) {
acc.push(...value);
}
return acc;
}, []),
};
}),
// Add value after index, similar to addGlobalValue, but for a speciic integration (index starts from 0)
addInternalValue: (index: number, integrationId: string, value: Values[]) =>
set((state) => {
const integrationIndex = state.internal.findIndex(
(i) => i.integration.id === integrationId
);
if (integrationIndex === -1) {
return {
internal: [
...state.internal,
{
integration: state.selectedIntegrations.find(
(i) => i.integration.id === integrationId
)!.integration,
integrationValue: value,
},
],
};
}
const updatedIntegration = state.internal[integrationIndex];
const newValues = updatedIntegration.integrationValue.reduce(
(acc, item, i) => {
acc.push(item);
if (i === index) {
acc.push(...value);
}
return acc;
},
[] as Values[]
);
return {
internal: state.internal.map((i, idx) =>
idx === integrationIndex ? { ...i, integrationValue: newValues } : i
),
};
}),
deleteGlobalValue: (index: number) =>
set((state) => ({
global: state.global.filter((_, i) => i !== index),
})),
deleteInternalValue: (integrationId: string, index: number) =>
set((state) => {
return {
internal: state.internal.map((i) => {
if (i.integration.id === integrationId) {
return {
...i,
integrationValue: i.integrationValue.filter(
(_, idx) => idx !== index
),
};
}
return i;
}),
};
}),
addRemoveInternal: (integrationId: string) =>
set((state) => {
const integration = state.selectedIntegrations.find(
(i) => i.integration.id === integrationId
);
const findIntegrationIndex = state.internal.findIndex(
(i) => i.integration.id === integrationId
);
if (findIntegrationIndex > -1) {
return {
internal: state.internal.filter(
(i) => i.integration.id !== integrationId
),
};
}
return {
internal: [
...state.internal,
{
integration: integration.integration,
integrationValue: state.global.slice(0).map((p) => p),
},
],
};
}),
changeOrderGlobal: (index: number, direction: 'up' | 'down') =>
set((state) => {
return {
global: arrayMoveImmutable(
state.global,
index,
direction === 'up' ? index - 1 : index + 1
),
};
}),
changeOrderInternal: (
integrationId: string,
index: number,
direction: 'up' | 'down'
) =>
set((state) => {
return {
internal: state.internal.map((item) => {
if (item.integration.id === integrationId) {
return {
...item,
integrationValue: arrayMoveImmutable(
item.integrationValue,
index,
direction === 'up' ? index - 1 : index + 1
),
};
}
return item;
}),
};
}),
setGlobalValueText: (index: number, content: string) =>
set((state) => ({
global: state.global.map((item, i) =>
i === index ? { ...item, content } : item
),
})),
setInternalValueMedia: (
integrationId: string,
index: number,
media: { id: string; path: string }[]
) => {
return set((state) => ({
internal: state.internal.map((item) =>
item.integration.id === integrationId
? {
...item,
integrationValue: item.integrationValue.map((v, i) =>
i === index ? { ...v, media } : v
),
}
: item
),
}));
},
setGlobalValueMedia: (index: number, media: { id: string; path: string }[]) =>
set((state) => ({
global: state.global.map((item, i) =>
i === index ? { ...item, media } : item
),
})),
addGlobalValueMedia: (index: number, media: { id: string; path: string }[]) =>
set((state) => ({
global: state.global.map((item, i) =>
i === index ? { ...item, media: [...item.media, ...media] } : item
),
})),
removeGlobalValueMedia: (index: number, mediaIndex: number) =>
set((state) => ({
global: state.global.map((item, i) =>
i === index
? {
...item,
media: item.media.filter((_, idx) => idx !== mediaIndex),
}
: item
),
})),
setInternalValueText: (
integrationId: string,
index: number,
content: string
) => {
set((state) => ({
internal: state.internal.map((item) =>
item.integration.id === integrationId
? {
...item,
integrationValue: item.integrationValue.map((v, i) =>
i === index ? { ...v, content } : v
),
}
: item
),
}));
},
addInternalValueMedia: (
integrationId: string,
index: number,
media: { id: string; path: string }[]
) =>
set((state) => ({
internal: state.internal.map((item) =>
item.integration.id === integrationId
? {
...item,
integrationValue: item.integrationValue.map((v, i) =>
i === index ? { ...v, media: [...v.media, ...media] } : v
),
}
: item
),
})),
removeInternalValueMedia: (
integrationId: string,
index: number,
mediaIndex: number
) =>
set((state) => ({
internal: state.internal.map((item) =>
item.integration.id === integrationId
? {
...item,
integrationValue: item.integrationValue.map((v, i) =>
i === index
? {
...v,
media: v.media.filter((_, idx) => idx !== mediaIndex),
}
: v
),
}
: item
),
})),
reset: () =>
set((state) => ({
...state,
...initialState,
})),
setAllIntegrations: (integrations: Integrations[]) =>
set((state) => ({
integrations: integrations,
})),
setTab: (tab: 0 | 1) =>
set((state) => ({
tab: tab,
})),
setLocked: (locked: boolean) =>
set((state) => ({
locked: locked,
})),
setHide: (hide: boolean) =>
set((state) => ({
hide: hide,
})),
setDate: (date: dayjs.Dayjs) =>
set((state) => ({
date,
})),
setRepeater: (repeater: number) =>
set((state) => ({
repeater,
})),
setTags: (tags: { label: string; value: string }[]) =>
set((state) => ({
tags,
})),
setIsCreateSet: (isCreateSet: boolean) =>
set((state) => ({
isCreateSet,
})),
setSelectedIntegrations: (
params: { selectedIntegrations: Integrations; settings: any }[]
) =>
set((state) => ({
selectedIntegrations: params.map((p) => ({
integration: p.selectedIntegrations,
settings: p.settings,
ref: createRef(),
})),
})),
setGlobalValue: (value: Values[]) =>
set((state) => ({
global: value,
})),
setInternalValue: (integrationId: string, value: Values[]) =>
set((state) => ({
internal: state.internal.map((item) =>
item.integration.id === integrationId
? { ...item, integrationValue: value }
: item
),
})),
}));

View File

@ -1,3 +1,5 @@
'use client';
import { FC, useCallback } from 'react';
import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
@ -91,7 +93,7 @@ export const UText: FC<{
return (
<div
onClick={mark}
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
>
<svg
width="25"

View File

@ -12,8 +12,8 @@ import { useToaster } from '@gitroom/react/toaster/toaster';
import clsx from 'clsx';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import dayjs from 'dayjs';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
const SaveSetModal: FC<{
postData: any;

View File

@ -23,7 +23,7 @@ export const SignatureBox: FC<{
)}
<div
onClick={addSignature}
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
>
<svg
width="25"

View File

@ -4,9 +4,9 @@ import 'reflect-metadata';
import { FC, useCallback } from 'react';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import dayjs from 'dayjs';
import { usePathname } from 'next/navigation';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
export const StandaloneModal: FC = () => {
const fetch = useFetch();
const params = usePathname();

View File

@ -165,6 +165,7 @@ checksums:
repeat_post_every: 0969cb627a580ec4afc19efd11958f4e
use_this_media: 6aeb1e22711eafe7f3dca7e496b52931
create_new_post: d0eba99343fb56e2baf90486eb68e70f
update_post: f24fceedd97a09c9b26bd7e1f8ce202c
merge_comments_into_one_post: 2f672bd0b317afb250671ad97ca881f6
accounts_that_will_engage: adb190c89e1a6f75cc883d904791581d
day: 47648cd60fc313bc3f05b70357a1d675
@ -190,6 +191,8 @@ checksums:
please_add_the_following_command_in_your_chat: 91cf09291659b1293786f0cf5ac50408
copy: 627c00d2c850b9b45f7341a6ac01b6bb
settings: 8df6777277469c1fd88cc18dde2f1cc3
integrations: 0ccce343287704cd90150c32e2fcad36
add_integration: 1b83c3002a8e84ba9449055de0f7e5ee
you_are_now_editing_only: 92478fd454abceccc5e47ddb543d9fec
tag_a_company: 6f8a005436febb76a4270a0ea6db432c
video_length_is_invalid_must_be_up_to: d5ab83e0f9d8c0f6e9c9ffdf7ad80c1d
@ -425,7 +428,7 @@ checksums:
enable_color_picker: 299af76a76f4bf348425e16cbef55cfa
cancel_the_color_picker: bb5a774d8367a5154e29ba7ee0615a12
no_content_yet: bdde7a6b531b27bd921bdbd26a0fada7
write_your_reply: b77b3540c5eb362704ec103120943d8a
write_your_reply: 0f4d124dbef4685590d5a95af3b5df64
add_a_tag: 1e529e5b2e62a9b521ded77f2f2cd53e
add_to_calendar: 40f6e2b51467f0abf819e69dcb3df2a1
select_channels_from_circles: ecde8aa1d62c31366127aa7b7a69b028
@ -488,3 +491,8 @@ checksums:
change_language: c798f65b78e23b2cf8fc29a1a24a182f
that_a_wrap: 0ecf5b5a1fbac9c2653f2642baf5d4a5
post_as_images_carousel: 2f82f0f6adbf03abfeec3389800d7232
save_set: e9c633bf57da897086a7bfd5108e445b
separate_post: 8c4bbcbabb5200898d1dec0fbdbc96a0
label_who_can_reply_to_this_post: 4d8913296a1fc3f197cb0aead34af73d
delete_integration: ccc879ccfcf7f85bcfe09f2bc3fa0dd3
start_writing_your_post: 471efc4f2a7e2cf02a065a2de34e7213

View File

@ -161,6 +161,7 @@
"repeat_post_every": "تكرار النشر كل...",
"use_this_media": "استخدم هذه الوسائط",
"create_new_post": "إنشاء منشور جديد",
"update_post": "تحديث المنشور الحالي",
"merge_comments_into_one_post": "دمج التعليقات في منشور واحد",
"accounts_that_will_engage": "الحسابات التي ستتفاعل:",
"day": "يوم",
@ -186,6 +187,8 @@
"please_add_the_following_command_in_your_chat": "يرجى إضافة الأمر التالي في الدردشة الخاصة بك:",
"copy": "نسخ",
"settings": "الإعدادات",
"integrations": "التكاملات",
"add_integration": "إضافة تكامل",
"you_are_now_editing_only": "أنت الآن تقوم بالتحرير فقط",
"tag_a_company": "الإشارة إلى شركة",
"video_length_is_invalid_must_be_up_to": "مدة الفيديو غير صالحة، يجب أن تكون حتى",
@ -421,7 +424,7 @@
"enable_color_picker": "تفعيل منتقي الألوان",
"cancel_the_color_picker": "إلغاء منتقي الألوان",
"no_content_yet": "لا يوجد محتوى بعد",
"write_your_reply": "اكتب ردك...",
"write_your_reply": "اكتب منشورك...",
"add_a_tag": "أضف وسمًا",
"add_to_calendar": "أضف إلى التقويم",
"select_channels_from_circles": "اختر القنوات من الدوائر أعلاه",
@ -483,5 +486,10 @@
"start_7_days_free_trial": "ابدأ تجربة مجانية لمدة 7 أيام",
"change_language": "تغيير اللغة",
"that_a_wrap": "انتهينا!\n\nإذا أعجبك هذا التسلسل:\n\n1. تابعني على @{{username}} للمزيد من هذه المواضيع\n2. أعد تغريد التغريدة أدناه لمشاركة هذا التسلسل مع جمهورك\n",
"post_as_images_carousel": "انشر كعرض شرائح للصور"
"post_as_images_carousel": "انشر كعرض شرائح للصور",
"save_set": "حفظ المجموعة",
"separate_post": "فصل المنشور إلى عدة منشورات",
"label_who_can_reply_to_this_post": "من يمكنه الرد على هذا المنشور؟",
"delete_integration": "حذف التكامل",
"start_writing_your_post": "ابدأ بكتابة منشورك لمعاينة"
}

View File

@ -161,6 +161,7 @@
"repeat_post_every": "প্রতি পোস্ট পুনরাবৃত্তি করুন...",
"use_this_media": "এই মিডিয়া ব্যবহার করুন",
"create_new_post": "নতুন পোস্ট তৈরি করুন",
"update_post": "বিদ্যমান পোস্ট আপডেট করুন",
"merge_comments_into_one_post": "মন্তব্যগুলি একটি পোস্টে একত্রিত করুন",
"accounts_that_will_engage": "যে অ্যাকাউন্টগুলি এনগেজ করবে:",
"day": "দিন",
@ -186,6 +187,8 @@
"please_add_the_following_command_in_your_chat": "অনুগ্রহ করে আপনার চ্যাটে নিম্নলিখিত কমান্ড যোগ করুন:",
"copy": "কপি করুন",
"settings": "সেটিংস",
"integrations": "ইন্টিগ্রেশনসমূহ",
"add_integration": "ইন্টিগ্রেশন যোগ করুন",
"you_are_now_editing_only": "আপনি এখন শুধুমাত্র সম্পাদনা করছেন",
"tag_a_company": "একটি কোম্পানি ট্যাগ করুন",
"video_length_is_invalid_must_be_up_to": "ভিডিওর দৈর্ঘ্য অবৈধ, সর্বোচ্চ হতে হবে",
@ -421,7 +424,7 @@
"enable_color_picker": "কলর পিকার সক্রিয় করা",
"cancel_the_color_picker": "কলর পিকার বাতিল করা",
"no_content_yet": "কন্টেন্ট নেই",
"write_your_reply": "আপনার উত্তর লিখুন",
"write_your_reply": "আপনার পোস্ট লিখুন...",
"add_a_tag": "ট্যাগ যোগ করুন",
"add_to_calendar": "ক্যালেন্ডারে যোগ করুন",
"select_channels_from_circles": "বলার মাধ্যমে চ্যানেল নির্বাচন করুন",
@ -483,5 +486,10 @@
"start_7_days_free_trial": " দিনের বিনামূল্যে ট্রায়াল শুরু করুন",
"change_language": "ভাষা পরিবর্তন করুন",
"that_a_wrap": "এটাই শেষ!\n\nযদি আপনি এই থ্রেডটি উপভোগ করে থাকেন:\n\n১. আরও এমন পোস্টের জন্য আমাকে @{{username}} ফলো করুন\n২. আপনার অডিয়েন্সের সাথে এই থ্রেডটি শেয়ার করতে নিচের টুইটটি রিটুইট করুন\n",
"post_as_images_carousel": "ছবির ক্যারোসেল হিসেবে পোস্ট করুন"
"post_as_images_carousel": "ছবির ক্যারোসেল হিসেবে পোস্ট করুন",
"save_set": "সেট সংরক্ষণ করুন",
"separate_post": "একটি পোস্টকে একাধিক পোস্টে ভাগ করুন",
"label_who_can_reply_to_this_post": "এই পোস্টে কে উত্তর দিতে পারবে?",
"delete_integration": "ইন্টিগ্রেশন মুছে ফেলুন",
"start_writing_your_post": "প্রিভিউর জন্য আপনার পোস্ট লেখা শুরু করুন"
}

View File

@ -161,6 +161,7 @@
"repeat_post_every": "Beitrag wiederholen alle...",
"use_this_media": "Dieses Medium verwenden",
"create_new_post": "Neuen Beitrag erstellen",
"update_post": "Vorhandenen Beitrag aktualisieren",
"merge_comments_into_one_post": "Kommentare zu einem Beitrag zusammenfassen",
"accounts_that_will_engage": "Konten, die interagieren werden:",
"day": "Tag",
@ -186,6 +187,8 @@
"please_add_the_following_command_in_your_chat": "Bitte fügen Sie den folgenden Befehl in Ihrem Chat hinzu:",
"copy": "Kopieren",
"settings": "Einstellungen",
"integrations": "Integrationen",
"add_integration": "Integration hinzufügen",
"you_are_now_editing_only": "Sie bearbeiten jetzt nur",
"tag_a_company": "Unternehmen markieren",
"video_length_is_invalid_must_be_up_to": "Videolänge ist ungültig, sie darf maximal",
@ -421,7 +424,7 @@
"enable_color_picker": "Farbwähler aktivieren",
"cancel_the_color_picker": "Farbwähler abbrechen",
"no_content_yet": "Noch kein Inhalt",
"write_your_reply": "Schreibe deine Antwort...",
"write_your_reply": "Schreibe deinen Beitrag...",
"add_a_tag": "Tag hinzufügen",
"add_to_calendar": "Zum Kalender hinzufügen",
"select_channels_from_circles": "Wähle Kanäle aus den obigen Kreisen aus",
@ -483,5 +486,10 @@
"start_7_days_free_trial": "7-tägige kostenlose Testversion starten",
"change_language": "Sprache ändern",
"that_a_wrap": "Das war's!\n\nWenn dir dieser Thread gefallen hat:\n\n1. Folge mir @{{username}} für mehr davon\n2. Retweete den untenstehenden Tweet, um diesen Thread mit deinem Publikum zu teilen\n",
"post_as_images_carousel": "Als Bilderkarussell posten"
"post_as_images_carousel": "Als Bilderkarussell posten",
"save_set": "Set speichern",
"separate_post": "Beitrag in mehrere Beiträge aufteilen",
"label_who_can_reply_to_this_post": "Wer kann auf diesen Beitrag antworten?",
"delete_integration": "Integration löschen",
"start_writing_your_post": "Beginne, deinen Beitrag für eine Vorschau zu schreiben"
}

View File

@ -161,6 +161,7 @@
"repeat_post_every": "Repeat Post Every...",
"use_this_media": "Use this media",
"create_new_post": "Create New Post",
"update_post": "Update Existing Post",
"merge_comments_into_one_post": "Merge comments into one post",
"accounts_that_will_engage": "Accounts that will engage:",
"day": "Day",
@ -423,7 +424,7 @@
"enable_color_picker": "Enable color picker",
"cancel_the_color_picker": "Cancel the color picker",
"no_content_yet": "No Content Yet",
"write_your_reply": "Write your reply...",
"write_your_reply": "Write your post...",
"add_a_tag": "Add a tag",
"add_to_calendar": "Add to Calendar",
"select_channels_from_circles": "Select channels from the circles above",
@ -489,5 +490,6 @@
"save_set": "Save Set",
"separate_post": "Separate post to multiple posts",
"label_who_can_reply_to_this_post": "Who can reply to this post?",
"delete_integration": "Delete Integration"
"delete_integration": "Delete Integration",
"start_writing_your_post": "Start writing your post for a preview"
}

View File

@ -161,6 +161,7 @@
"repeat_post_every": "Repetir publicación cada...",
"use_this_media": "Usar este medio",
"create_new_post": "Crear nueva publicación",
"update_post": "Actualizar publicación existente",
"merge_comments_into_one_post": "Unir comentarios en una sola publicación",
"accounts_that_will_engage": "Cuentas que interactuarán:",
"day": "Día",
@ -186,6 +187,8 @@
"please_add_the_following_command_in_your_chat": "Por favor agrega el siguiente comando en tu chat:",
"copy": "Copiar",
"settings": "Configuración",
"integrations": "Integraciones",
"add_integration": "Agregar integración",
"you_are_now_editing_only": "Ahora solo estás editando",
"tag_a_company": "Etiquetar una empresa",
"video_length_is_invalid_must_be_up_to": "La duración del video no es válida, debe ser de hasta",
@ -421,7 +424,7 @@
"enable_color_picker": "Habilitar selector de color",
"cancel_the_color_picker": "Cancelar el selector de color",
"no_content_yet": "Aún no hay contenido",
"write_your_reply": "Escribe tu respuesta...",
"write_your_reply": "Escribe tu publicación...",
"add_a_tag": "Agregar una etiqueta",
"add_to_calendar": "Agregar al calendario",
"select_channels_from_circles": "Selecciona canales de los círculos de arriba",
@ -483,5 +486,10 @@
"start_7_days_free_trial": "Comienza la prueba gratuita de 7 días",
"change_language": "Cambiar idioma",
"that_a_wrap": "¡Eso es todo!\n\nSi te gustó este hilo:\n\n1. Sígueme en @{{username}} para más contenido como este\n2. Haz RT al tuit de abajo para compartir este hilo con tu audiencia\n",
"post_as_images_carousel": "Publicar como carrusel de imágenes"
"post_as_images_carousel": "Publicar como carrusel de imágenes",
"save_set": "Guardar conjunto",
"separate_post": "Separar publicación en varias publicaciones",
"label_who_can_reply_to_this_post": "¿Quién puede responder a esta publicación?",
"delete_integration": "Eliminar integración",
"start_writing_your_post": "Comienza a escribir tu publicación para obtener una vista previa"
}

View File

@ -161,6 +161,7 @@
"repeat_post_every": "Répéter la publication tous les...",
"use_this_media": "Utiliser ce média",
"create_new_post": "Créer une nouvelle publication",
"update_post": "Mettre à jour le post existant",
"merge_comments_into_one_post": "Fusionner les commentaires en une seule publication",
"accounts_that_will_engage": "Comptes qui vont interagir :",
"day": "Jour",
@ -186,6 +187,8 @@
"please_add_the_following_command_in_your_chat": "Veuillez ajouter la commande suivante dans votre chat :",
"copy": "Copier",
"settings": "Paramètres",
"integrations": "Intégrations",
"add_integration": "Ajouter une intégration",
"you_are_now_editing_only": "Vous modifiez maintenant uniquement",
"tag_a_company": "Taguer une entreprise",
"video_length_is_invalid_must_be_up_to": "La durée de la vidéo est invalide, elle doit être au maximum de",
@ -421,7 +424,7 @@
"enable_color_picker": "Activer le sélecteur de couleurs",
"cancel_the_color_picker": "Annuler le sélecteur de couleurs",
"no_content_yet": "Pas encore de contenu",
"write_your_reply": "Écrivez votre réponse...",
"write_your_reply": "Écrivez votre post...",
"add_a_tag": "Ajouter une étiquette",
"add_to_calendar": "Ajouter au calendrier",
"select_channels_from_circles": "Sélectionnez des canaux à partir des cercles ci-dessus",
@ -483,5 +486,10 @@
"start_7_days_free_trial": "Commencez lessai gratuit de 7 jours",
"change_language": "Changer de langue",
"that_a_wrap": "C'est terminé !\n\nSi vous avez aimé ce fil :\n\n1. Suivez-moi @{{username}} pour en voir d'autres\n2. Retweetez le tweet ci-dessous pour partager ce fil avec votre audience\n",
"post_as_images_carousel": "Publier en carrousel dimages"
"post_as_images_carousel": "Publier en carrousel dimages",
"save_set": "Enregistrer l'ensemble",
"separate_post": "Séparer le post en plusieurs publications",
"label_who_can_reply_to_this_post": "Qui peut répondre à ce post ?",
"delete_integration": "Supprimer l'intégration",
"start_writing_your_post": "Commencez à écrire votre post pour un aperçu"
}

Some files were not shown because too many files have changed in this diff Show More