feat: plugs
This commit is contained in:
commit
7ca7a14294
|
|
@ -0,0 +1,19 @@
|
|||
name: "🙏🏻 Installation Problem"
|
||||
description: "Report an issue with installation"
|
||||
title: "Installation Problem"
|
||||
labels: ["type: installation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: For installation issues, please visit our https://discord.postiz.com for assistance.
|
||||
description: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
|
||||
placeholder: |
|
||||
For installation issues, please visit our https://discord.postiz.com for assistance.
|
||||
Please do not save this issue - do not submit installation issues on GitHub.
|
||||
|
||||
24
README.md
24
README.md
|
|
@ -1,8 +1,14 @@
|
|||
<p align="center">
|
||||
<a href="https://affiliate.postiz.com">
|
||||
<img src="https://github.com/user-attachments/assets/af9f47b3-e20c-402b-bd11-02f39248d738" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://postiz.com" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/765e9d72-3ee7-4a56-9d59-a2c9befe2311">
|
||||
<img alt="Novu Logo" src="https://github.com/user-attachments/assets/f0d30d70-dddb-4142-8876-e9aa6ed1cb99" width="280"/>
|
||||
<img alt="Postiz Logo" src="https://github.com/user-attachments/assets/f0d30d70-dddb-4142-8876-e9aa6ed1cb99" width="280"/>
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -58,22 +64,6 @@
|
|||
|
||||
<br />
|
||||
|
||||
|
||||
<p align="center">
|
||||
<br /><br /><br />
|
||||
<h1>We participate in Hacktoberfest 2024! 🎉🎊</h1>
|
||||
<p align="left">We are sending a t-shirt for every merged PR! (max 1 per person)</p>
|
||||
<p align="left"><strong>Rules:</strong></p>
|
||||
<ul align="left">
|
||||
<li>You must create an issue before making a pull request.</li>
|
||||
<li>You can also ask to be assigned to an issue. During Hacktoberfest, each issue can have multiple assignees.</li>
|
||||
<li>We have to approve the issue and add a "hacktoberfest" tag.</li>
|
||||
<li>We encourage everybody to contribute to all types of issues. We will only send swag for issues with features and bug fixes (no typos, sorry).</li>
|
||||
</ul>
|
||||
<p align="center"><img align="center" width="400" src="https://github.com/user-attachments/assets/3ceffccc-e4b3-4098-b9ba-44a94cf01294" /></p>
|
||||
<br /><br /><br />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<video src="https://github.com/user-attachments/assets/05436a01-19c8-4827-b57f-05a5e7637a67" width="100%" />
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,12 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { NotEnoughScopes } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import {
|
||||
NotEnoughScopes,
|
||||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -38,6 +42,37 @@ export class IntegrationsController {
|
|||
return this._integrationManager.getAllIntegrations();
|
||||
}
|
||||
|
||||
@Get('/customers')
|
||||
getCustomers(@GetOrgFromRequest() org: Organization) {
|
||||
return this._integrationService.customers(org.id);
|
||||
}
|
||||
|
||||
@Put('/:id/group')
|
||||
async updateIntegrationGroup(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { group: string }
|
||||
) {
|
||||
return this._integrationService.updateIntegrationGroup(
|
||||
org.id,
|
||||
id,
|
||||
body.group
|
||||
);
|
||||
}
|
||||
|
||||
@Put('/:id/customer-name')
|
||||
async updateOnCustomerName(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { name: string }
|
||||
) {
|
||||
return this._integrationService.updateOnCustomerName(
|
||||
org.id,
|
||||
id,
|
||||
body.name
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
async getIntegrationList(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
|
|
@ -52,7 +87,7 @@ export class IntegrationsController {
|
|||
id: p.id,
|
||||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
picture: p.picture,
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
refreshNeeded: p.refreshNeeded,
|
||||
|
|
@ -61,6 +96,7 @@ export class IntegrationsController {
|
|||
time: JSON.parse(p.postingTimes),
|
||||
changeProfilePicture: !!findIntegration?.changeProfilePicture,
|
||||
changeNickName: !!findIntegration?.changeNickname,
|
||||
customer: p.customer,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -202,11 +238,51 @@ export class IntegrationsController {
|
|||
}
|
||||
|
||||
if (integrationProvider[body.name]) {
|
||||
return integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
getIntegration.internalId
|
||||
);
|
||||
try {
|
||||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
getIntegration.internalId
|
||||
);
|
||||
|
||||
return load;
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
const { accessToken, refreshToken, expiresIn } =
|
||||
await integrationProvider.refreshToken(
|
||||
getIntegration.refreshToken
|
||||
);
|
||||
|
||||
if (accessToken) {
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
getIntegration.organizationId,
|
||||
getIntegration.name,
|
||||
getIntegration.picture!,
|
||||
'social',
|
||||
getIntegration.internalId,
|
||||
getIntegration.providerIdentifier,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn
|
||||
);
|
||||
|
||||
getIntegration.token = accessToken;
|
||||
|
||||
if (integrationProvider.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
return this.functionIntegration(org, body);
|
||||
} else {
|
||||
await this._integrationService.disconnectChannel(
|
||||
org.id,
|
||||
getIntegration
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
throw new Error('Function not found');
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
|
|
@ -10,7 +10,6 @@ html {
|
|||
}
|
||||
.box {
|
||||
position: relative;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.box span {
|
||||
position: relative;
|
||||
|
|
@ -385,3 +384,14 @@ div div .set-font-family {
|
|||
font-style: normal !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.col-calendar:hover:before {
|
||||
content: "Date passed";
|
||||
color: white;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
opacity: 30%;
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import clsx from 'clsx';
|
|||
import { VariableContextComponent } from '@gitroom/react/helpers/variable.context';
|
||||
import { Fragment } from 'react';
|
||||
import { PHProvider } from '@gitroom/react/helpers/posthog';
|
||||
import UtmSaver from '@gitroom/helpers/utils/utm.saver';
|
||||
import { ToltScript } from '@gitroom/frontend/components/layout/tolt.script';
|
||||
|
||||
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
|
||||
|
||||
|
|
@ -25,7 +27,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
<head>
|
||||
<link
|
||||
rel="icon"
|
||||
href={!!process.env.IS_GENERAL ? '/favicon.png' : '/postiz-fav.png'}
|
||||
href="/favicon.ico"
|
||||
sizes="any"
|
||||
/>
|
||||
</head>
|
||||
|
|
@ -41,7 +43,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
frontEndUrl={process.env.FRONTEND_URL!}
|
||||
isGeneral={!!process.env.IS_GENERAL}
|
||||
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
|
||||
tolt={process.env.NEXT_PUBLIC_TOLT!}
|
||||
>
|
||||
<ToltScript />
|
||||
<Plausible
|
||||
domain={!!process.env.IS_GENERAL ? 'postiz.com' : 'gitroom.com'}
|
||||
>
|
||||
|
|
@ -49,6 +53,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
phkey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
|
||||
host={process.env.NEXT_PUBLIC_POSTHOG_HOST}
|
||||
>
|
||||
<UtmSaver />
|
||||
<LayoutContext>{children}</LayoutContext>
|
||||
</PHProvider>
|
||||
</Plausible>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import DrawChart from 'chart.js/auto';
|
||||
import { TotalList } from '@gitroom/frontend/components/analytics/stars.and.forks.interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { chunk } from 'lodash';
|
||||
|
||||
function mergeDataPoints(data: TotalList[], numPoints: number): TotalList[] {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import DrawChart from 'chart.js/auto';
|
||||
import {
|
||||
ForksList,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { sortBy } from 'lodash';
|
||||
import { Track } from '@gitroom/react/form/track';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Subscription } from '@prisma/client';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
|
@ -21,10 +19,11 @@ import interClass from '@gitroom/react/helpers/inter.font';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { AddProviderComponent } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
import { useUtmUrl } from '@gitroom/helpers/utils/utm.saver';
|
||||
import { useTolt } from '@gitroom/frontend/components/layout/tolt.script';
|
||||
|
||||
export interface Tiers {
|
||||
month: Array<{
|
||||
|
|
@ -222,6 +221,8 @@ export const MainBillingComponent: FC<{
|
|||
const user = useUser();
|
||||
const modal = useModals();
|
||||
const router = useRouter();
|
||||
const utm = useUtmUrl();
|
||||
const tolt = useTolt();
|
||||
|
||||
const [subscription, setSubscription] = useState<Subscription | undefined>(
|
||||
sub
|
||||
|
|
@ -347,7 +348,9 @@ export const MainBillingComponent: FC<{
|
|||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
period: monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY',
|
||||
utm,
|
||||
billing,
|
||||
tolt: tolt()
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
|
@ -389,7 +392,7 @@ export const MainBillingComponent: FC<{
|
|||
|
||||
setLoading(false);
|
||||
},
|
||||
[monthlyOrYearly, subscription, user]
|
||||
[monthlyOrYearly, subscription, user, utm]
|
||||
);
|
||||
|
||||
if (user?.isLifetime) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@ import { CopilotPopup } from '@copilotkit/react-ui';
|
|||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import Image from 'next/image';
|
||||
import { weightedLength } from '@gitroom/helpers/utils/count.length';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
|
||||
function countCharacters(text: string, type: string): number {
|
||||
if (type !== 'x') {
|
||||
return text.length;
|
||||
}
|
||||
|
||||
return weightedLength(text);
|
||||
}
|
||||
|
||||
export const AddEditModal: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
|
|
@ -56,17 +67,36 @@ export const AddEditModal: FC<{
|
|||
reopenModal: () => void;
|
||||
mutate: () => void;
|
||||
}> = (props) => {
|
||||
const { date, integrations, reopenModal, mutate } = props;
|
||||
const [dateState, setDateState] = useState(date);
|
||||
|
||||
// hook to open a new modal
|
||||
const modal = useModals();
|
||||
const { date, integrations: ints, reopenModal, mutate } = props;
|
||||
const [customer, setCustomer] = useState('');
|
||||
|
||||
// selected integrations to allow edit
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
|
||||
Integrations[]
|
||||
>([]);
|
||||
|
||||
const integrations = useMemo(() => {
|
||||
if (!customer) {
|
||||
return ints;
|
||||
}
|
||||
|
||||
const list = ints.filter((f) => f?.customer?.id === customer);
|
||||
if (list.length === 1) {
|
||||
setSelectedIntegrations([list[0]]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [customer, ints]);
|
||||
|
||||
const totalCustomers = useMemo(() => {
|
||||
return uniqBy(ints, (i) => i?.customer?.id).length;
|
||||
}, [ints]);
|
||||
|
||||
const [dateState, setDateState] = useState(date);
|
||||
|
||||
// hook to open a new modal
|
||||
const modal = useModals();
|
||||
|
||||
// value of each editor
|
||||
const [value, setValue] = useState<
|
||||
Array<{
|
||||
|
|
@ -267,7 +297,8 @@ export const AddEditModal: FC<{
|
|||
for (const key of allKeys) {
|
||||
if (key.checkValidity) {
|
||||
const check = await key.checkValidity(
|
||||
key?.value.map((p: any) => p.image || [])
|
||||
key?.value.map((p: any) => p.image || []),
|
||||
key.settings
|
||||
);
|
||||
if (typeof check === 'string') {
|
||||
toaster.show(check, 'warning');
|
||||
|
|
@ -276,9 +307,12 @@ export const AddEditModal: FC<{
|
|||
}
|
||||
|
||||
if (
|
||||
key.value.some(
|
||||
(p) => p.content.length > (key.maximumCharacters || 1000000)
|
||||
)
|
||||
key.value.some((p) => {
|
||||
return (
|
||||
countCharacters(p.content, key?.integration?.identifier || '') >
|
||||
(key.maximumCharacters || 1000000)
|
||||
);
|
||||
})
|
||||
) {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
|
|
@ -386,14 +420,15 @@ export const AddEditModal: FC<{
|
|||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx('flex p-[10px] rounded-[4px] bg-primary gap-[20px]')}
|
||||
id="add-edit-modal"
|
||||
className={clsx(
|
||||
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
|
||||
!expend.expend
|
||||
? 'flex-1 w-1 animate-overflow'
|
||||
: 'w-0 overflow-hidden'
|
||||
!expend.expend ? 'flex-1 animate-overflow' : 'w-0 overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0">
|
||||
|
|
@ -404,6 +439,26 @@ export const AddEditModal: FC<{
|
|||
information={data}
|
||||
onChange={setPostFor}
|
||||
/>
|
||||
{totalCustomers > 1 && (
|
||||
<Select
|
||||
hideErrors={true}
|
||||
label=""
|
||||
name="customer"
|
||||
value={customer}
|
||||
onChange={(e) => {
|
||||
setCustomer(e.target.value);
|
||||
setSelectedIntegrations([]);
|
||||
}}
|
||||
disableForm={true}
|
||||
>
|
||||
<option value="">Selected Customer</option>
|
||||
{uniqBy(ints, (u) => u?.customer?.name).map((p) => (
|
||||
<option key={p.customer?.id} value={p.customer?.id}>
|
||||
Customer: {p.customer?.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<DatePicker onChange={setDateState} date={dateState} />
|
||||
</div>
|
||||
</TopTitle>
|
||||
|
|
@ -419,7 +474,7 @@ export const AddEditModal: FC<{
|
|||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500'
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
|
|
@ -539,13 +594,13 @@ export const AddEditModal: FC<{
|
|||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative h-[68px] flex flex-col rounded-[4px] border border-customColor6 bg-sixth">
|
||||
<div className="flex flex-1 gap-[10px] relative">
|
||||
<div className="absolute w-full h-full flex gap-[10px] justify-end items-center right-[16px]">
|
||||
<Button
|
||||
className="bg-transparent text-inputText"
|
||||
onClick={askClose}
|
||||
>
|
||||
<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 pr-[16px]">
|
||||
<div
|
||||
id="add-edit-post-dialog-buttons"
|
||||
className="flex flex-row flex-wrap w-full h-full gap-[10px] justify-end items-center"
|
||||
>
|
||||
<Button className="rounded-[4px]" onClick={askClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Submitted
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import { Input } from '@gitroom/react/form/input';
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import {
|
||||
MediaComponent,
|
||||
showMediaBox,
|
||||
} from '@gitroom/frontend/components/media/media.component';
|
||||
import { showMediaBox } from '@gitroom/frontend/components/media/media.component';
|
||||
|
||||
export const BotPicture: FC<{
|
||||
integration: Integrations;
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ import { useSearchParams } from 'next/navigation';
|
|||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(weekOfYear);
|
||||
import { extend } from 'dayjs';
|
||||
extend(isoWeek);
|
||||
extend(weekOfYear);
|
||||
|
||||
export const CalendarContext = createContext({
|
||||
currentDay: dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6,
|
||||
|
|
@ -61,6 +62,10 @@ export interface Integrations {
|
|||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
time: { time: number }[];
|
||||
customer?: {
|
||||
name?: string;
|
||||
id?: string;
|
||||
}
|
||||
}
|
||||
|
||||
function getWeekNumber(date: Date) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import clsx from 'clsx';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
|
||||
import { Integration, Post, State } from '@prisma/client';
|
||||
import { useAddProvider } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { CommentComponent } from '@gitroom/frontend/components/launches/comments/comment.component';
|
||||
|
|
@ -26,8 +25,19 @@ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
|||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
import Image from 'next/image';
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
import { extend } from 'dayjs';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
import removeMd from 'remove-markdown';
|
||||
extend(isSameOrAfter);
|
||||
extend(isSameOrBefore);
|
||||
|
||||
const convertTimeFormatBasedOnLocality = (time: number) => {
|
||||
if (isUSCitizen()) {
|
||||
return `${time === 12 ? 12 : time % 12}:00 ${time >= 12 ? 'PM' : 'AM'}`;
|
||||
} else {
|
||||
return `${time}:00`;
|
||||
}
|
||||
};
|
||||
|
||||
export const days = [
|
||||
'Monday',
|
||||
|
|
@ -90,7 +100,7 @@ export const DayView = () => {
|
|||
.startOf('day')
|
||||
.add(option[0].time, 'minute')
|
||||
.local()
|
||||
.format('HH:mm')}
|
||||
.format(isUSCitizen() ? 'hh:mm A' : 'HH:mm')}
|
||||
</div>
|
||||
<div
|
||||
key={option[0].time}
|
||||
|
|
@ -139,7 +149,8 @@ export const WeekView = () => {
|
|||
{hours.map((hour) => (
|
||||
<Fragment key={hour}>
|
||||
<div className="p-2 pr-4 bg-secondary text-center items-center justify-center flex">
|
||||
{hour.toString().padStart(2, '0')}:00
|
||||
{/* {hour.toString().padStart(2, '0')}:00 */}
|
||||
{convertTimeFormatBasedOnLocality(hour)}
|
||||
</div>
|
||||
{days.map((day, indexDay) => (
|
||||
<Fragment key={`${day}-${hour}`}>
|
||||
|
|
@ -230,7 +241,7 @@ export const Calendar = () => {
|
|||
const { display } = useCalendar();
|
||||
|
||||
return (
|
||||
<DNDProvider>
|
||||
<>
|
||||
{display === 'day' ? (
|
||||
<DayView />
|
||||
) : display === 'week' ? (
|
||||
|
|
@ -238,7 +249,7 @@ export const Calendar = () => {
|
|||
) : (
|
||||
<MonthView />
|
||||
)}
|
||||
</DNDProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -432,8 +443,9 @@ export const CalendarColumn: FC<{
|
|||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex flex-col flex-1',
|
||||
canDrop && 'bg-white/80'
|
||||
'relative flex flex-col flex-1 text-white',
|
||||
canDrop && 'bg-white/80',
|
||||
isBeforeNow && postList.length === 0 && 'cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -444,8 +456,9 @@ export const CalendarColumn: FC<{
|
|||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'flex-col text-[12px] pointer w-full cursor-pointer overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow && 'bg-customColor23 flex-1',
|
||||
'flex-col text-[12px] pointer w-full overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow ? 'bg-customColor23 flex-1' : 'cursor-pointer',
|
||||
isBeforeNow && postList.length === 0 && 'col-calendar',
|
||||
canBeTrending && 'bg-customColor24'
|
||||
)}
|
||||
>
|
||||
|
|
@ -499,7 +512,10 @@ export const CalendarColumn: FC<{
|
|||
className={`w-full h-full rounded-[10px] hover:border hover:border-seventh flex justify-center items-center gap-[20px] opacity-30 grayscale hover:grayscale-0 hover:opacity-100`}
|
||||
>
|
||||
{integrations.map((selectedIntegrations) => (
|
||||
<div className="relative" key={selectedIntegrations.identifier}>
|
||||
<div
|
||||
className="relative"
|
||||
key={selectedIntegrations.identifier}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500'
|
||||
|
|
@ -589,7 +605,7 @@ const CalendarItem: FC<{
|
|||
</div>
|
||||
<div className="whitespace-pre-wrap line-clamp-3">
|
||||
{state === 'DRAFT' ? 'Draft: ' : ''}
|
||||
{post.content}
|
||||
{removeMd(post.content).replace(/\n/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Autocomplete } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
||||
export const CustomerModal: FC<{
|
||||
integration: Integration & { customer?: { id: string; name: string } };
|
||||
onClose: () => void;
|
||||
}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const { onClose, integration } = props;
|
||||
const [customer, setCustomer] = useState(
|
||||
integration.customer?.name || undefined
|
||||
);
|
||||
const modal = useModals();
|
||||
|
||||
const loadCustomers = useCallback(async () => {
|
||||
return (await fetch('/integrations/customers')).json();
|
||||
}, []);
|
||||
|
||||
const removeFromCustomer = useCallback(async () => {
|
||||
saveCustomer(true);
|
||||
}, []);
|
||||
|
||||
const saveCustomer = useCallback(async (removeCustomer?: boolean) => {
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`/integrations/${integration.id}/customer-name`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: removeCustomer ? '' : customer }),
|
||||
});
|
||||
|
||||
modal.closeAll();
|
||||
onClose();
|
||||
}, [customer]);
|
||||
|
||||
const { data } = useSWR('/customers', loadCustomers);
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
|
||||
<TopTitle title={`Move / Add to customer`} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={() => modal.closeAll()}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="mt-[16px]">
|
||||
<Autocomplete
|
||||
value={customer}
|
||||
onChange={setCustomer}
|
||||
classNames={{
|
||||
label: 'text-white',
|
||||
}}
|
||||
label="Select Customer"
|
||||
placeholder="Start typing..."
|
||||
data={data?.map((p: any) => p.name) || []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="my-[16px] flex gap-[10px]">
|
||||
<Button onClick={() => saveCustomer()}>Save</Button>
|
||||
{!!integration?.customer?.name && <Button className="bg-red-700" onClick={removeFromCustomer}>Remove from customer</Button>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { forwardRef, useCallback, useRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { MDEditorProps } from '@uiw/react-md-editor/src/Types';
|
||||
import { RefMDEditor } from '@uiw/react-md-editor/src/Editor';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import dayjs from 'dayjs';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useCalendar } from '@gitroom/frontend/components/launches/calendar.cont
|
|||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
|
||||
export const Filters = () => {
|
||||
const week = useCalendar();
|
||||
|
|
@ -12,30 +13,30 @@ export const Filters = () => {
|
|||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
: week.display === 'week'
|
||||
? dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.startOf('isoWeek')
|
||||
.format('DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.endOf('isoWeek')
|
||||
.format('DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
: dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.startOf('month')
|
||||
.format('DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.endOf('month')
|
||||
.format('DD/MM/YYYY');
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY');
|
||||
|
||||
const setDay = useCallback(() => {
|
||||
week.setFilters({
|
||||
|
|
@ -145,74 +146,78 @@ export const Filters = () => {
|
|||
week.currentDay,
|
||||
]);
|
||||
return (
|
||||
<div className="text-textColor flex gap-[8px] items-center select-none">
|
||||
<div onClick={previous} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[80px] text-center">
|
||||
{week.display === 'day'
|
||||
? `${dayjs()
|
||||
.month(week.currentMonth)
|
||||
.week(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('dddd')}`
|
||||
: week.display === 'week'
|
||||
? `Week ${week.currentWeek}`
|
||||
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
|
||||
</div>
|
||||
<div onClick={next} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">{betweenDates}</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'day' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setDay}
|
||||
>
|
||||
Day
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'week' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setWeek}
|
||||
>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'month' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setMonth}
|
||||
>
|
||||
Month
|
||||
</div>
|
||||
<div className="text-textColor flex flex-col md:flex-row gap-[8px] items-center select-none">
|
||||
<div className = "flex flex-grow flex-row">
|
||||
<div onClick={previous} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[80px] text-center">
|
||||
{week.display === 'day'
|
||||
? `${dayjs()
|
||||
.month(week.currentMonth)
|
||||
.week(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('dddd')}`
|
||||
: week.display === 'week'
|
||||
? `Week ${week.currentWeek}`
|
||||
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
|
||||
</div>
|
||||
<div onClick={next} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">{betweenDates}</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'day' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setDay}
|
||||
>
|
||||
Day
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'week' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setWeek}
|
||||
>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'month' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setMonth}
|
||||
>
|
||||
Month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
|||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import clsx from 'clsx';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
import { Chakra_Petch } from 'next/font/google';
|
||||
import { FC } from 'react';
|
||||
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
|
||||
import { textSlicer } from '@gitroom/helpers/utils/count.length';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
|
||||
export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
|
|
@ -14,12 +14,13 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, props.maximumCharacters || 10000) + '<mark class="bg-red-500" data-tooltip-id="tooltip" data-tooltip-content="This text will be cropped">' + text?.slice(props.maximumCharacters || 10000) + '</mark>';
|
||||
const {start, end} = textSlicer(integration?.identifier || '', props.maximumCharacters || 10000, text);
|
||||
return text.slice(start, end) + '<mark class="bg-red-500" data-tooltip-id="tooltip" data-tooltip-content="This text will be cropped">' + text?.slice(end) + '</mark>';
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx('w-[555px] px-[16px]')}>
|
||||
<div className={clsx('w-full md:w-[555px] px-[16px]')}>
|
||||
<div className="w-full h-full relative flex flex-col">
|
||||
{newValues.map((value, index) => (
|
||||
<div
|
||||
|
|
@ -62,7 +63,7 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
{integration?.display || '@username'}
|
||||
</div>
|
||||
</div>
|
||||
<pre className={clsx('text-wrap', chakra.className)} dangerouslySetInnerHTML={{__html: value.text}} />
|
||||
<pre className={clsx('text-wrap', interClass)} dangerouslySetInnerHTML={{__html: value.text}} />
|
||||
{!!value?.images?.length && (
|
||||
<div className={clsx("w-full rounded-[16px] overflow-hidden mt-[12px]", value?.images?.length > 3 ? 'grid grid-cols-2 gap-[4px]' : 'flex gap-[4px]')}>
|
||||
{value.images.map((image, index) => (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import dayjs from 'dayjs';
|
|||
import { Calendar, TimeInput } from '@mantine/dates';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { isUSCitizen } from './isuscitizen.utils';
|
||||
|
||||
export const DatePicker: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
|
|
@ -39,7 +40,7 @@ export const DatePicker: FC<{
|
|||
onClick={changeShow}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="cursor-pointer">{date.format('DD/MM/YYYY HH:mm')}</div>
|
||||
<div className="cursor-pointer">{date.format(isUSCitizen() ? 'MM/DD/YYYY hh:mm A' : 'DD/MM/YYYY HH:mm')}</div>
|
||||
<div className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
export const isUSCitizen = () => {
|
||||
const userLanguage = navigator.language || navigator.languages[0];
|
||||
return userLanguage.startsWith('en-US')
|
||||
}
|
||||
|
|
@ -13,8 +13,7 @@ import {
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import dayjs from 'dayjs';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
const postUrlEmitter = new EventEmitter();
|
||||
|
||||
|
|
@ -78,26 +77,32 @@ export const LinkedinCompany: FC<{
|
|||
const { onClose, onSelect, id } = props;
|
||||
const fetch = useFetch();
|
||||
const [company, setCompany] = useState<any>(null);
|
||||
const toast = useToaster();
|
||||
|
||||
const getCompany = async () => {
|
||||
if (!company) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
const {options} = await (
|
||||
await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: 'company',
|
||||
data: {
|
||||
url: company,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
onSelect(options.value);
|
||||
onClose();
|
||||
try {
|
||||
const { options } = await (
|
||||
await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: 'company',
|
||||
data: {
|
||||
url: company,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
onSelect(options.value);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast.show('Failed to load profile', 'warning');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export const useFormatting = (
|
|||
if (params.removeMarkdown) {
|
||||
newText = removeMd(newText);
|
||||
}
|
||||
newText = newText.replace(/@\w{1,15}/g, function(match) {
|
||||
return `<strong>${match}</strong>`;
|
||||
});
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ const finalInformation = {} as {
|
|||
settings: () => object;
|
||||
trigger: () => Promise<boolean>;
|
||||
isValid: boolean;
|
||||
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>;
|
||||
checkValidity?: (
|
||||
value: Array<Array<{ path: string }>>,
|
||||
settings: any
|
||||
) => Promise<string | true>;
|
||||
maximumCharacters?: number;
|
||||
};
|
||||
};
|
||||
|
|
@ -18,8 +21,11 @@ export const useValues = (
|
|||
identifier: string,
|
||||
value: Array<{ id?: string; content: string; media?: Array<string> }>,
|
||||
dto: any,
|
||||
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>,
|
||||
maximumCharacters?: number,
|
||||
checkValidity?: (
|
||||
value: Array<Array<{ path: string }>>,
|
||||
settings: any
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number
|
||||
) => {
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(dto);
|
||||
|
|
@ -43,8 +49,7 @@ export const useValues = (
|
|||
finalInformation[integration].trigger = form.trigger;
|
||||
|
||||
if (checkValidity) {
|
||||
finalInformation[integration].checkValidity =
|
||||
checkValidity;
|
||||
finalInformation[integration].checkValidity = checkValidity;
|
||||
}
|
||||
|
||||
if (maximumCharacters) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { AddProviderButton } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { orderBy } from 'lodash';
|
||||
// import { Calendar } from '@gitroom/frontend/components/launches/calendar';
|
||||
import { groupBy, orderBy } from 'lodash';
|
||||
import { CalendarWeekProvider } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { Filters } from '@gitroom/frontend/components/launches/filters';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
@ -19,7 +18,201 @@ import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
|||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
import { Calendar } from './calendar';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
|
||||
|
||||
interface MenuComponentInterface {
|
||||
refreshChannel: (
|
||||
integration: Integration & { identifier: string }
|
||||
) => () => void;
|
||||
continueIntegration: (integration: Integration) => () => void;
|
||||
totalNonDisabledChannels: number;
|
||||
mutate: (shouldReload?: boolean) => void;
|
||||
update: (shouldReload: boolean) => void;
|
||||
}
|
||||
|
||||
export const MenuGroupComponent: FC<
|
||||
MenuComponentInterface & {
|
||||
changeItemGroup: (id: string, group: string) => void;
|
||||
group: {
|
||||
id: string;
|
||||
name: string;
|
||||
values: Array<
|
||||
Integration & {
|
||||
identifier: string;
|
||||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
}
|
||||
> = (props) => {
|
||||
const {
|
||||
group,
|
||||
mutate,
|
||||
update,
|
||||
continueIntegration,
|
||||
totalNonDisabledChannels,
|
||||
refreshChannel,
|
||||
changeItemGroup,
|
||||
} = props;
|
||||
|
||||
const [collectedProps, drop] = useDrop(() => ({
|
||||
accept: 'menu',
|
||||
drop: (item: { id: string }, monitor) => {
|
||||
changeItemGroup(item.id, group.id);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: !!monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="gap-[16px] flex flex-col relative"
|
||||
// @ts-ignore
|
||||
ref={drop}
|
||||
>
|
||||
{collectedProps.isOver && (
|
||||
<div className="absolute left-0 top-0 w-full h-full pointer-events-none">
|
||||
<div className="w-full h-full left-0 top-0 relative">
|
||||
<div className="bg-white/30 w-full h-full p-[8px] box-content rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!group.name && <div>{group.name}</div>}
|
||||
{group.values.map((integration) => (
|
||||
<MenuComponent
|
||||
key={integration.id}
|
||||
integration={integration}
|
||||
mutate={mutate}
|
||||
continueIntegration={continueIntegration}
|
||||
update={update}
|
||||
refreshChannel={refreshChannel}
|
||||
totalNonDisabledChannels={totalNonDisabledChannels}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const MenuComponent: FC<
|
||||
MenuComponentInterface & {
|
||||
integration: Integration & {
|
||||
identifier: string;
|
||||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
};
|
||||
}
|
||||
> = (props) => {
|
||||
const {
|
||||
totalNonDisabledChannels,
|
||||
continueIntegration,
|
||||
refreshChannel,
|
||||
mutate,
|
||||
update,
|
||||
integration,
|
||||
} = props;
|
||||
|
||||
const user = useUser();
|
||||
const [collected, drag, dragPreview] = useDrag(() => ({
|
||||
type: 'menu',
|
||||
item: { id: integration.id },
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
// @ts-ignore
|
||||
ref={dragPreview}
|
||||
{...(integration.refreshNeeded && {
|
||||
onClick: refreshChannel(integration),
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content': 'Channel disconnected, click to reconnect.',
|
||||
})}
|
||||
key={integration.id}
|
||||
className={clsx(
|
||||
'flex gap-[8px] items-center',
|
||||
integration.refreshNeeded && 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer"
|
||||
onClick={
|
||||
integration.refreshNeeded
|
||||
? refreshChannel(integration)
|
||||
: continueIntegration(integration)
|
||||
}
|
||||
>
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture!}
|
||||
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] -right-[5px]"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
// @ts-ignore
|
||||
ref={drag}
|
||||
{...(integration.disabled &&
|
||||
totalNonDisabledChannels === user?.totalChannels
|
||||
? {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'This channel is disabled, please upgrade your plan to enable it.',
|
||||
}
|
||||
: {})}
|
||||
role="Handle"
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden cursor-move',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
<Menu
|
||||
canChangeProfilePicture={integration.changeProfilePicture}
|
||||
canChangeNickName={integration.changeNickName}
|
||||
mutate={mutate}
|
||||
onChange={update}
|
||||
id={integration.id}
|
||||
canEnable={
|
||||
user?.totalChannels! > totalNonDisabledChannels &&
|
||||
integration.disabled
|
||||
}
|
||||
canDisable={!integration.disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const LaunchesComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
|
|
@ -32,8 +225,6 @@ export const LaunchesComponent = () => {
|
|||
return (await (await fetch(path)).json()).integrations;
|
||||
}, []);
|
||||
|
||||
const user = useUser();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
data: integrations,
|
||||
|
|
@ -49,6 +240,28 @@ export const LaunchesComponent = () => {
|
|||
);
|
||||
}, [integrations]);
|
||||
|
||||
const changeItemGroup = useCallback(
|
||||
async (id: string, group: string) => {
|
||||
mutate(
|
||||
integrations.map((integration: any) => {
|
||||
if (integration.id === id) {
|
||||
return { ...integration, customer: { id: group } };
|
||||
}
|
||||
return integration;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await fetch(`/integrations/${id}/group`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ group }),
|
||||
});
|
||||
|
||||
mutate();
|
||||
},
|
||||
[integrations]
|
||||
);
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
integrations,
|
||||
|
|
@ -57,6 +270,25 @@ export const LaunchesComponent = () => {
|
|||
);
|
||||
}, [integrations]);
|
||||
|
||||
const menuIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
Object.values(
|
||||
groupBy(sortedIntegrations, (o) => o?.customer?.id || '')
|
||||
).map((p) => ({
|
||||
name: (p[0].customer?.name || '') as string,
|
||||
id: (p[0].customer?.id || '') as string,
|
||||
isEmpty: p.length === 0,
|
||||
values: orderBy(
|
||||
p,
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
),
|
||||
})),
|
||||
['isEmpty', 'name'],
|
||||
['desc', 'asc']
|
||||
);
|
||||
}, [sortedIntegrations]);
|
||||
|
||||
const update = useCallback(async (shouldReload: boolean) => {
|
||||
if (shouldReload) {
|
||||
setReload(true);
|
||||
|
|
@ -114,117 +346,41 @@ export const LaunchesComponent = () => {
|
|||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<CalendarWeekProvider integrations={sortedIntegrations}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="outline-none w-full h-full grid grid-cols-[220px_minmax(0,1fr)] gap-[30px] scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
|
||||
<div className="w-[220px] bg-third p-[16px] flex flex-col gap-[24px] min-h-[100%]">
|
||||
<h2 className="text-[20px]">Channels</h2>
|
||||
<div className="gap-[16px] flex flex-col">
|
||||
{sortedIntegrations.length === 0 && (
|
||||
<div className="text-[12px]">No channels</div>
|
||||
)}
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<div
|
||||
{...(integration.refreshNeeded && {
|
||||
onClick: refreshChannel(integration),
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'Channel disconnected, click to reconnect.',
|
||||
})}
|
||||
key={integration.id}
|
||||
className={clsx(
|
||||
'flex gap-[8px] items-center',
|
||||
integration.refreshNeeded && 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps ||
|
||||
integration.refreshNeeded) && (
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer"
|
||||
onClick={
|
||||
integration.refreshNeeded
|
||||
? refreshChannel(integration)
|
||||
: continueIntegration(integration)
|
||||
}
|
||||
>
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
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] -right-[5px]"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
{...(integration.disabled &&
|
||||
totalNonDisabledChannels === user?.totalChannels
|
||||
? {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'This channel is disabled, please upgrade your plan to enable it.',
|
||||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
<Menu
|
||||
canChangeProfilePicture={integration.changeProfilePicture}
|
||||
canChangeNickName={integration.changeNickName}
|
||||
<DNDProvider>
|
||||
<CalendarWeekProvider integrations={sortedIntegrations}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="outline-none w-full h-full grid grid-cols[1fr] md:grid-cols-[220px_minmax(0,1fr)] gap-[30px] scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
|
||||
<div className="bg-third p-[16px] flex flex-col gap-[24px] min-h-[100%]">
|
||||
<h2 className="text-[20px]">Channels</h2>
|
||||
<div className="gap-[16px] flex flex-col select-none">
|
||||
{sortedIntegrations.length === 0 && (
|
||||
<div className="text-[12px]">No channels</div>
|
||||
)}
|
||||
{menuIntegrations.map((menu) => (
|
||||
<MenuGroupComponent
|
||||
changeItemGroup={changeItemGroup}
|
||||
key={menu.name}
|
||||
group={menu}
|
||||
mutate={mutate}
|
||||
onChange={update}
|
||||
id={integration.id}
|
||||
canEnable={
|
||||
user?.totalChannels! > totalNonDisabledChannels &&
|
||||
integration.disabled
|
||||
}
|
||||
canDisable={!integration.disabled}
|
||||
continueIntegration={continueIntegration}
|
||||
update={update}
|
||||
refreshChannel={refreshChannel}
|
||||
totalNonDisabledChannels={totalNonDisabledChannels}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{/*{sortedIntegrations?.length > 0 && user?.tier?.ai && <GeneratorComponent />}*/}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
<Calendar />
|
||||
</div>
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{/*{sortedIntegrations?.length > 0 && user?.tier?.ai && <GeneratorComponent />}*/}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
<Calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CalendarWeekProvider>
|
||||
</CalendarWeekProvider>
|
||||
</DNDProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useModals } from '@mantine/modals';
|
|||
import { TimeTable } from '@gitroom/frontend/components/launches/time.table';
|
||||
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { BotPicture } from '@gitroom/frontend/components/launches/bot.picture';
|
||||
import { CustomerModal } from '@gitroom/frontend/components/launches/customer.modal';
|
||||
|
||||
export const Menu: FC<{
|
||||
canEnable: boolean;
|
||||
|
|
@ -36,10 +37,13 @@ export const Menu: FC<{
|
|||
setShow(false);
|
||||
});
|
||||
|
||||
const changeShow: MouseEventHandler<HTMLDivElement> = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
setShow(!show);
|
||||
}, [show]);
|
||||
const changeShow: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
setShow(!show);
|
||||
},
|
||||
[show]
|
||||
);
|
||||
|
||||
const disableChannel = useCallback(async () => {
|
||||
if (
|
||||
|
|
@ -139,6 +143,34 @@ export const Menu: FC<{
|
|||
setShow(false);
|
||||
}, [integrations]);
|
||||
|
||||
const addToCustomer = useCallback(() => {
|
||||
const findIntegration = integrations.find(
|
||||
(integration) => integration.id === id
|
||||
);
|
||||
|
||||
modal.openModal({
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[600px] bg-transparent text-textColor',
|
||||
},
|
||||
size: '100%',
|
||||
withCloseButton: false,
|
||||
closeOnEscape: true,
|
||||
closeOnClickOutside: true,
|
||||
children: (
|
||||
<CustomerModal
|
||||
// @ts-ignore
|
||||
integration={findIntegration}
|
||||
onClose={() => {
|
||||
mutate();
|
||||
toast.show('Customer Updated', 'success');
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
setShow(false);
|
||||
}, [integrations]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer relative select-none"
|
||||
|
|
@ -192,6 +224,23 @@ export const Menu: FC<{
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-[12px] items-center" onClick={addToCustomer}>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M31.9997 17C31.9997 17.2652 31.8943 17.5196 31.7068 17.7071C31.5192 17.8946 31.2649 18 30.9997 18H28.9997V20C28.9997 20.2652 28.8943 20.5196 28.7068 20.7071C28.5192 20.8946 28.2649 21 27.9997 21C27.7345 21 27.4801 20.8946 27.2926 20.7071C27.105 20.5196 26.9997 20.2652 26.9997 20V18H24.9997C24.7345 18 24.4801 17.8946 24.2926 17.7071C24.105 17.5196 23.9997 17.2652 23.9997 17C23.9997 16.7348 24.105 16.4804 24.2926 16.2929C24.4801 16.1054 24.7345 16 24.9997 16H26.9997V14C26.9997 13.7348 27.105 13.4804 27.2926 13.2929C27.4801 13.1054 27.7345 13 27.9997 13C28.2649 13 28.5192 13.1054 28.7068 13.2929C28.8943 13.4804 28.9997 13.7348 28.9997 14V16H30.9997C31.2649 16 31.5192 16.1054 31.7068 16.2929C31.8943 16.4804 31.9997 16.7348 31.9997 17ZM24.7659 24.3562C24.9367 24.5595 25.0197 24.8222 24.9967 25.0866C24.9737 25.351 24.8466 25.5955 24.6434 25.7662C24.4402 25.937 24.1775 26.02 23.9131 25.997C23.6486 25.974 23.4042 25.847 23.2334 25.6437C20.7184 22.6487 17.2609 21 13.4997 21C9.73843 21 6.28093 22.6487 3.76593 25.6437C3.59519 25.8468 3.35079 25.9737 3.08648 25.9966C2.82217 26.0194 2.55961 25.9364 2.35655 25.7656C2.15349 25.5949 2.02658 25.3505 2.00372 25.0862C1.98087 24.8219 2.06394 24.5593 2.23468 24.3562C4.10218 22.1337 6.42468 20.555 9.00593 19.71C7.43831 18.7336 6.23133 17.2733 5.56759 15.5498C4.90386 13.8264 4.81949 11.9337 5.32724 10.1581C5.83499 8.38242 6.90724 6.82045 8.38176 5.70847C9.85629 4.59649 11.6529 3.995 13.4997 3.995C15.3465 3.995 17.1431 4.59649 18.6176 5.70847C20.0921 6.82045 21.1644 8.38242 21.6721 10.1581C22.1799 11.9337 22.0955 13.8264 21.4318 15.5498C20.768 17.2733 19.561 18.7336 17.9934 19.71C20.5747 20.555 22.8972 22.1337 24.7659 24.3562ZM13.4997 19C14.7853 19 16.042 18.6188 17.1109 17.9045C18.1798 17.1903 19.0129 16.1752 19.5049 14.9874C19.9969 13.7997 20.1256 12.4928 19.8748 11.2319C19.624 9.97103 19.0049 8.81284 18.0959 7.9038C17.1868 6.99476 16.0286 6.37569 14.7678 6.12489C13.5069 5.87409 12.2 6.00281 11.0122 6.49478C9.82451 6.98675 8.80935 7.81987 8.09512 8.88879C7.38089 9.95771 6.99968 11.2144 6.99968 12.5C7.00166 14.2233 7.68712 15.8754 8.90567 17.094C10.1242 18.3126 11.7764 18.998 13.4997 19Z"
|
||||
fill="green"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]">Move / add to customer</div>
|
||||
</div>
|
||||
<div className="flex gap-[12px] items-center" onClick={editTimeTable}>
|
||||
<div>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -28,12 +28,16 @@ import { newImage } from '@gitroom/frontend/components/launches/helpers/new.imag
|
|||
import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
|
||||
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { linkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
|
||||
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 { useModals } from '@mantine/modals';
|
||||
|
||||
// Simple component to change back to settings on after changing tab
|
||||
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
|
||||
|
|
@ -68,15 +72,16 @@ export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
|||
return children;
|
||||
};
|
||||
|
||||
export const withProvider = (
|
||||
SettingsComponent: FC<{values?: any}> | null,
|
||||
CustomPreviewComponent?: FC<{maximumCharacters?: number}>,
|
||||
export const withProvider = function <T extends object>(
|
||||
SettingsComponent: FC<{ values?: any }> | null,
|
||||
CustomPreviewComponent?: FC<{ maximumCharacters?: number }>,
|
||||
dto?: any,
|
||||
checkValidity?: (
|
||||
value: Array<Array<{ path: string }>>
|
||||
value: Array<Array<{ path: string }>>,
|
||||
settings: T
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number
|
||||
) => {
|
||||
) {
|
||||
return (props: {
|
||||
identifier: string;
|
||||
id: string;
|
||||
|
|
@ -90,6 +95,8 @@ export const withProvider = (
|
|||
}) => {
|
||||
const existingData = useExistingData();
|
||||
const { integration, date } = useIntegration();
|
||||
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
|
||||
|
||||
useCopilotReadable({
|
||||
description:
|
||||
integration?.type === 'social'
|
||||
|
|
@ -254,6 +261,21 @@ export const withProvider = (
|
|||
},
|
||||
});
|
||||
|
||||
const tagPersonOrCompany = useCallback(
|
||||
(integration: string, editor: (value: string) => void) => () => {
|
||||
setShowLinkedinPopUp(
|
||||
<LinkedinCompany
|
||||
onSelect={(tag) => {
|
||||
editor(tag);
|
||||
}}
|
||||
id={integration}
|
||||
onClose={() => setShowLinkedinPopUp(false)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// this is a trick to prevent the data from being deleted, yet we don't render the elements
|
||||
if (!props.show) {
|
||||
return null;
|
||||
|
|
@ -262,6 +284,7 @@ export const withProvider = (
|
|||
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]">
|
||||
|
|
@ -318,6 +341,20 @@ export const withProvider = (
|
|||
<div>
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 text-textColor editor">
|
||||
{integration?.identifier === 'linkedin' && (
|
||||
<Button
|
||||
className="mb-[5px]"
|
||||
onClick={tagPersonOrCompany(
|
||||
integration.id,
|
||||
(newValue: string) =>
|
||||
changeValue(index)(
|
||||
val.content + newValue
|
||||
)
|
||||
)}
|
||||
>
|
||||
Tag a company
|
||||
</Button>
|
||||
)}
|
||||
<Editor
|
||||
order={index}
|
||||
height={InPlaceValue.length > 1 ? 200 : 250}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/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';
|
||||
|
||||
const postType = [
|
||||
{
|
||||
value: 'post',
|
||||
label: 'Post / Reel',
|
||||
},
|
||||
{
|
||||
value: 'story',
|
||||
label: 'Story',
|
||||
},
|
||||
];
|
||||
const InstagramCollaborators: FC<{ values?: any }> = (props) => {
|
||||
const { watch, register, formState, control } = useSettings();
|
||||
const postCurrentType = watch('post_type');
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
label="Post Type"
|
||||
{...register('post_type', {
|
||||
value: 'post',
|
||||
})}
|
||||
>
|
||||
<option value="">Select Post Type...</option>
|
||||
{postType.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{postCurrentType !== 'story' && (
|
||||
<InstagramCollaboratorsTags
|
||||
label="Collaborators (max 3) - accounts can't be private"
|
||||
{...register('collaborators')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider<InstagramDto>(
|
||||
InstagramCollaborators,
|
||||
undefined,
|
||||
InstagramDto,
|
||||
async ([firstPost, ...otherPosts], settings) => {
|
||||
if (!firstPost.length) {
|
||||
return 'Instagram should have at least one media';
|
||||
}
|
||||
|
||||
if (firstPost.length > 1 && settings.post_type === 'story') {
|
||||
return 'Instagram stories can only have one media';
|
||||
}
|
||||
|
||||
const checkVideosLength = await Promise.all(
|
||||
firstPost
|
||||
.filter((f) => f.path.indexOf('mp4') > -1)
|
||||
.flatMap((p) => p.path)
|
||||
.map((p) => {
|
||||
return new Promise<number>((res) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.src = p;
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
res(video.duration);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
for (const video of checkVideosLength) {
|
||||
if (video > 60 && settings.post_type === 'story') {
|
||||
return 'Instagram stories should be maximum 60 seconds';
|
||||
}
|
||||
|
||||
if (video > 90 && settings.post_type === 'post') {
|
||||
return 'Instagram reel should be maximum 90 seconds';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
2200
|
||||
);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async ([firstPost, ...otherPosts]) => {
|
||||
if (!firstPost.length) {
|
||||
return 'Instagram should have at least one media';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
2200
|
||||
);
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
|
||||
export const InstagramCollaboratorsTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: { target: { value: any[]; name: string } }) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const { getValues } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<string>('');
|
||||
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({ target: { value: modify, name } });
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 3) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({ target: { value: modify, name } });
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const suggestionsArray = useMemo(() => {
|
||||
return [...tagValue, { label: suggestions, value: suggestions }].filter(f => f.label);
|
||||
}, [suggestions, tagValue]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
placeholderText="Add a tag"
|
||||
suggestions={suggestionsArray}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onInput={setSuggestions}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ import RedditProvider from "@gitroom/frontend/components/launches/providers/redd
|
|||
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.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';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,52 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
export default withProvider(null, undefined, undefined, async (posts) => {
|
||||
if (posts.some(p => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async (posts) => {
|
||||
if (posts.some((p) => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
|
||||
return true;
|
||||
}, 280);
|
||||
if (
|
||||
posts.some(
|
||||
(p) => p.some((m) => m.path.indexOf('mp4') > -1) && p.length > 1
|
||||
)
|
||||
) {
|
||||
return 'There can be maximum 1 video in a post.';
|
||||
}
|
||||
|
||||
for (const load of posts.flatMap((p) => p.flatMap((a) => a.path))) {
|
||||
if (load.indexOf('mp4') > -1) {
|
||||
const isValid = await checkVideoDuration(load);
|
||||
if (!isValid) {
|
||||
return 'Video duration must be less than or equal to 140 seconds.';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
280
|
||||
);
|
||||
|
||||
const checkVideoDuration = async (url: string): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.src = url;
|
||||
video.preload = 'metadata';
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
// Check if the duration is less than or equal to 140 seconds
|
||||
const duration = video.duration;
|
||||
if (duration <= 140) {
|
||||
resolve(true); // Video duration is acceptable
|
||||
} else {
|
||||
resolve(false); // Video duration exceeds 140 seconds
|
||||
}
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
reject(new Error('Failed to load video metadata.'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const YoutubeSettings: FC = () => {
|
|||
const { register, control } = useSettings();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Input label="Title" {...register('title')} />
|
||||
<Input label="Title" {...register('title')} maxLength={100} />
|
||||
<Select label="Type" {...register('type', { value: 'public' })}>
|
||||
{type.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export const Impersonate = () => {
|
|||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="px-[23px]">
|
||||
<div className="md:px-[23px]">
|
||||
<div className="bg-forth h-[52px] flex justify-center items-center border-input border rounded-[8px]">
|
||||
<div className="relative flex flex-col w-[600px]">
|
||||
<div className="relative z-[999]">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { ReactNode, useCallback } from 'react';
|
||||
import { FetchWrapperComponent } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { isGeneral } from '@gitroom/react/helpers/is.general';
|
||||
import { useReturnUrl } from '@gitroom/frontend/app/auth/return.url.component';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import NotificationComponent from '@gitroom/frontend/components/notifications/no
|
|||
import Link from 'next/link';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
|
|
@ -37,10 +36,12 @@ const ModeComponent = dynamic(
|
|||
{ ssr: false }
|
||||
);
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(isBetween);
|
||||
import { extend } from 'dayjs';
|
||||
|
||||
extend(utc);
|
||||
extend(weekOfYear);
|
||||
extend(isoWeek);
|
||||
extend(isBetween);
|
||||
|
||||
export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
||||
const fetch = useFetch();
|
||||
|
|
@ -77,12 +78,12 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-textColor flex flex-col">
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary sm:px-6 px-0 text-textColor flex flex-col">
|
||||
{user?.admin && <Impersonate />}
|
||||
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<nav className="px-0 md:px-[23px] gap-2 grid grid-rows-[repeat(2,_auto)] grid-cols-2 md:grid-rows-1 md:grid-cols-[repeat(3,_auto)] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-2xl flex items-center gap-[10px] text-textColor"
|
||||
className="text-2xl flex items-center gap-[10px] text-textColor order-1"
|
||||
>
|
||||
<div className="min-w-[55px]">
|
||||
<Image
|
||||
|
|
@ -93,9 +94,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
!isGeneral ? 'mt-[12px]' : 'min-w-[80px]'
|
||||
)}
|
||||
className={clsx(!isGeneral ? 'mt-[12px]' : 'min-w-[80px]')}
|
||||
>
|
||||
{isGeneral ? (
|
||||
<svg
|
||||
|
|
@ -127,21 +126,22 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{user?.orgId && (user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
|
||||
{user?.orgId &&
|
||||
(user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
|
||||
<TopMenu />
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<div id = "systray-buttons" className="flex items-center justify-self-end gap-[8px] order-2 md:order-3">
|
||||
<ModeComponent />
|
||||
<SettingsComponent />
|
||||
<NotificationComponent />
|
||||
<OrganizationSelector />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col">
|
||||
{(user.tier === 'FREE' && isGeneral) && billingEnabled ? (
|
||||
<div className="flex-1 rounded-3xl px-0 md:px-[23px] py-[17px] flex flex-col">
|
||||
{user.tier === 'FREE' && isGeneral && billingEnabled ? (
|
||||
<>
|
||||
<div className="text-center mb-[20px] text-xl">
|
||||
<h1 className="text-3xl">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
'use client';
|
||||
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import Script from 'next/script';
|
||||
|
||||
export const useTolt = () => {
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
return window?.tolt_referral || '';
|
||||
};
|
||||
};
|
||||
|
||||
export const ToltScript = () => {
|
||||
const { tolt } = useVariables();
|
||||
if (!tolt) return null;
|
||||
return (
|
||||
<Script
|
||||
async={true}
|
||||
src="https://cdn.tolt.io/tolt.js"
|
||||
data-tolt={tolt}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -55,6 +55,13 @@ export const useMenuItems = () => {
|
|||
role: ['ADMIN', 'SUPERADMIN'],
|
||||
requireBilling: true,
|
||||
},
|
||||
{
|
||||
name: 'Affiliate',
|
||||
icon: 'affiliate',
|
||||
path: 'https://affiliate.postiz.com',
|
||||
role: ['ADMIN', 'SUPERADMIN', 'USER'],
|
||||
requireBilling: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
|
@ -65,8 +72,8 @@ export const TopMenu: FC = () => {
|
|||
const menuItems = useMenuItems();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full animate-normalFadeDown">
|
||||
<ul className="gap-5 flex flex-1 items-center text-[18px]">
|
||||
<div className="flex flex-col h-full animate-normalFadeDown order-3 md:order-2 col-span-2 md:col-span-1">
|
||||
<ul className="gap-0 md:gap-5 flex flex-1 items-center text-[18px]">
|
||||
{menuItems
|
||||
.filter((f) => {
|
||||
if (f.requireBilling && !billingEnabled) {
|
||||
|
|
@ -81,9 +88,10 @@ export const TopMenu: FC = () => {
|
|||
<li key={item.name}>
|
||||
<Link
|
||||
prefetch={true}
|
||||
target={item.path.indexOf('http') > -1 ? '_blank' : '_self'}
|
||||
href={item.path}
|
||||
className={clsx(
|
||||
'flex gap-2 items-center box',
|
||||
'flex gap-2 items-center box px-[6px] md:px-[24px] py-[8px]',
|
||||
menuItems
|
||||
.filter((f) => {
|
||||
if (f.role) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'reflect-metadata';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { Post as PrismaPost } from '.prisma/client';
|
||||
import { Providers } from '@gitroom/frontend/components/launches/providers/show.all.providers';
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const NotificationOpenComponent = () => {
|
|||
const { data, isLoading } = useSWR('notifications', loadNotifications);
|
||||
|
||||
return (
|
||||
<div className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] min-h-[200px] top-[100%] right-0 bg-third text-textColor rounded-[16px] flex flex-col border border-tableBorder">
|
||||
<div id="notification-popup" className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] min-h-[200px] top-[100%] right-0 bg-third text-textColor rounded-[16px] flex flex-col border border-tableBorder z-[2]">
|
||||
<div className={`p-[16px] border-b border-tableBorder ${interClass} font-bold`}>
|
||||
Notifications
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import React, {
|
|||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { Integration } from '@prisma/client';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ export const Plugs = () => {
|
|||
};
|
||||
}, [currentIntegration, plugList]);
|
||||
|
||||
console.log(currentIntegrationPlug);
|
||||
if (isLoading || plugLoading) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -83,9 +82,9 @@ export const Plugs = () => {
|
|||
<img src="/peoplemarketplace.svg" />
|
||||
</div>
|
||||
<div className="text-[48px]">
|
||||
Can{"'"}t show analytics yet
|
||||
There are not plugs matching your channels
|
||||
<br />
|
||||
You have to add Social Media channels
|
||||
You have to add: X or LinkedIn or Threads
|
||||
</div>
|
||||
<Button onClick={() => router.push('/launches')}>
|
||||
Go to the calendar to add channels
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ export const TeamsComponent = () => {
|
|||
<Button
|
||||
className={`!bg-customColor3 !h-[24px] border border-customColor21 rounded-[4px] text-[12px] ${interClass}`}
|
||||
onClick={remove(p)}
|
||||
secondary={true}
|
||||
>
|
||||
<div className="flex justify-center items-center gap-[4px]">
|
||||
<div>
|
||||
|
|
@ -222,7 +223,7 @@ export const TeamsComponent = () => {
|
|||
>
|
||||
<path
|
||||
d="M11.8125 3.125H9.625V2.6875C9.625 2.3394 9.48672 2.00556 9.24058 1.75942C8.99444 1.51328 8.6606 1.375 8.3125 1.375H5.6875C5.3394 1.375 5.00556 1.51328 4.75942 1.75942C4.51328 2.00556 4.375 2.3394 4.375 2.6875V3.125H2.1875C2.07147 3.125 1.96019 3.17109 1.87814 3.25314C1.79609 3.33519 1.75 3.44647 1.75 3.5625C1.75 3.67853 1.79609 3.78981 1.87814 3.87186C1.96019 3.95391 2.07147 4 2.1875 4H2.625V11.875C2.625 12.1071 2.71719 12.3296 2.88128 12.4937C3.04538 12.6578 3.26794 12.75 3.5 12.75H10.5C10.7321 12.75 10.9546 12.6578 11.1187 12.4937C11.2828 12.3296 11.375 12.1071 11.375 11.875V4H11.8125C11.9285 4 12.0398 3.95391 12.1219 3.87186C12.2039 3.78981 12.25 3.67853 12.25 3.5625C12.25 3.44647 12.2039 3.33519 12.1219 3.25314C12.0398 3.17109 11.9285 3.125 11.8125 3.125ZM5.25 2.6875C5.25 2.57147 5.29609 2.46019 5.37814 2.37814C5.46019 2.29609 5.57147 2.25 5.6875 2.25H8.3125C8.42853 2.25 8.53981 2.29609 8.62186 2.37814C8.70391 2.46019 8.75 2.57147 8.75 2.6875V3.125H5.25V2.6875ZM10.5 11.875H3.5V4H10.5V11.875ZM6.125 6.1875V9.6875C6.125 9.80353 6.07891 9.91481 5.99686 9.99686C5.91481 10.0789 5.80353 10.125 5.6875 10.125C5.57147 10.125 5.46019 10.0789 5.37814 9.99686C5.29609 9.91481 5.25 9.80353 5.25 9.6875V6.1875C5.25 6.07147 5.29609 5.96019 5.37814 5.87814C5.46019 5.79609 5.57147 5.75 5.6875 5.75C5.80353 5.75 5.91481 5.79609 5.99686 5.87814C6.07891 5.96019 6.125 6.07147 6.125 6.1875ZM8.75 6.1875V9.6875C8.75 9.80353 8.70391 9.91481 8.62186 9.99686C8.53981 10.0789 8.42853 10.125 8.3125 10.125C8.19647 10.125 8.08519 10.0789 8.00314 9.99686C7.92109 9.91481 7.875 9.80353 7.875 9.6875V6.1875C7.875 6.07147 7.92109 5.96019 8.00314 5.87814C8.08519 5.79609 8.19647 5.75 8.3125 5.75C8.42853 5.75 8.53981 5.79609 8.62186 5.87814C8.70391 5.96019 8.75 6.07147 8.75 6.1875Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,21 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { EventPattern, Transport } from '@nestjs/microservices';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
|
||||
@Controller()
|
||||
export class PlugsController {
|
||||
constructor(
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
|
||||
@EventPattern('plugs', Transport.REDIS)
|
||||
async plug(data: {
|
||||
orgId: string;
|
||||
integrationId: string;
|
||||
funcName: string;
|
||||
retry: number;
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
try {
|
||||
await this._integrationService.startPlug(data);
|
||||
|
||||
if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') {
|
||||
return this._workerServiceProducer.emit('plugs', {
|
||||
id: data.integrationId + '-' + data.funcName,
|
||||
options: {
|
||||
delay: 6000, // delay,
|
||||
},
|
||||
payload: {
|
||||
...data,
|
||||
retry: data.retry,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (data.retry > 3) {
|
||||
return;
|
||||
}
|
||||
return this._workerServiceProducer.emit('plugs', {
|
||||
id: data.integrationId + '-' + data.funcName,
|
||||
options: {
|
||||
delay: data.delay, // delay,
|
||||
},
|
||||
payload: {
|
||||
...data,
|
||||
retry: data.retry + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
return this._integrationService.processPlugs(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,7 @@ export class ConfigurationChecker {
|
|||
this.checkIsValidUrl('FRONTEND_URL')
|
||||
this.checkIsValidUrl('NEXT_PUBLIC_BACKEND_URL')
|
||||
this.checkIsValidUrl('BACKEND_INTERNAL_URL')
|
||||
this.checkNonEmpty('CLOUDFLARE_ACCOUNT_ID', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_ACCESS_KEY', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_SECRET_ACCESS_KEY', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_BUCKETNAME', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_BUCKET_URL', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_REGION', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('STORAGE_PROVIDER', 'Needed to setup storage.')
|
||||
}
|
||||
|
||||
checkNonEmpty (key: string, description?: string): boolean {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
export function Plug(params: {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
runEveryMilliseconds: number;
|
||||
totalRuns: number;
|
||||
fields: {
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
// @ts-ignore
|
||||
import twitter from 'twitter-text';
|
||||
|
||||
export const textSlicer = (
|
||||
integrationType: string,
|
||||
end: number,
|
||||
text: string
|
||||
): {start: number, end: number} => {
|
||||
if (integrationType !== 'x') {
|
||||
return {
|
||||
start: 0,
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
const {validRangeEnd, valid} = twitter.parseTweet(text);
|
||||
return {
|
||||
start: 0,
|
||||
end: valid ? end : validRangeEnd
|
||||
}
|
||||
};
|
||||
|
||||
export const weightedLength = (text: string): number => {
|
||||
return twitter.parseTweet(text).weightedLength;
|
||||
}
|
||||
|
|
@ -1,47 +1,35 @@
|
|||
import {FC, useCallback, useEffect} from "react";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
|
||||
const UtmSaver: FC = () => {
|
||||
const query = useSearchParams();
|
||||
useEffect(() => {
|
||||
const landingUrl = localStorage.getItem('landingUrl');
|
||||
if (landingUrl) {
|
||||
return ;
|
||||
}
|
||||
const query = useSearchParams();
|
||||
const [value, setValue] = useLocalStorage({ key: 'utm', defaultValue: '' });
|
||||
|
||||
localStorage.setItem('landingUrl', window.location.href);
|
||||
localStorage.setItem('referrer', document.referrer);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const landingUrl = localStorage.getItem('landingUrl');
|
||||
if (landingUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const utm = query.get('utm_source') || query.get('utm');
|
||||
const utmMedium = query.get('utm_medium');
|
||||
const utmCampaign = query.get('utm_campaign');
|
||||
localStorage.setItem('landingUrl', window.location.href);
|
||||
localStorage.setItem('referrer', document.referrer);
|
||||
}, []);
|
||||
|
||||
if (utm) {
|
||||
localStorage.setItem('utm', utm);
|
||||
}
|
||||
if (utmMedium) {
|
||||
localStorage.setItem('utm_medium', utmMedium);
|
||||
}
|
||||
if (utmCampaign) {
|
||||
localStorage.setItem('utm_campaign', utmCampaign);
|
||||
}
|
||||
}, [query]);
|
||||
useEffect(() => {
|
||||
const utm = query.get('utm_source') || query.get('utm') || query.get('ref');
|
||||
if (utm && !value) {
|
||||
setValue(utm);
|
||||
}
|
||||
}, [query, value]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const useUtmSaver = () => {
|
||||
return useCallback(() => {
|
||||
return {
|
||||
utm: localStorage.getItem('utm'),
|
||||
utmMedium: localStorage.getItem('utm_medium'),
|
||||
utmCampaign: localStorage.getItem('utm_campaign'),
|
||||
landingUrl: localStorage.getItem('landingUrl'),
|
||||
referrer: localStorage.getItem('referrer'),
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default UtmSaver;
|
||||
export const useUtmUrl = () => {
|
||||
const [value] = useLocalStorage({ key: 'utm', defaultValue: '' });
|
||||
return value || '';
|
||||
};
|
||||
export default UtmSaver;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export class IntegrationRepository {
|
|||
private _integration: PrismaRepository<'integration'>,
|
||||
private _posts: PrismaRepository<'post'>,
|
||||
private _plugs: PrismaRepository<'plugs'>,
|
||||
private _exisingPlugData: PrismaRepository<'exisingPlugData'>
|
||||
private _exisingPlugData: PrismaRepository<'exisingPlugData'>,
|
||||
private _customers: PrismaRepository<'customer'>
|
||||
) {}
|
||||
|
||||
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
|
||||
|
|
@ -32,6 +33,35 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getPlug(plugId: string) {
|
||||
return this._plugs.model.plugs.findFirst({
|
||||
where: {
|
||||
id: plugId,
|
||||
},
|
||||
include: {
|
||||
integration: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getPlugs(orgId: string, integrationId: string) {
|
||||
return this._plugs.model.plugs.findMany({
|
||||
where: {
|
||||
integrationId,
|
||||
organizationId: orgId,
|
||||
activated: true,
|
||||
},
|
||||
include: {
|
||||
integration: {
|
||||
select: {
|
||||
id: true,
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateIntegration(id: string, params: Partial<Integration>) {
|
||||
if (
|
||||
params.picture &&
|
||||
|
|
@ -66,7 +96,7 @@ export class IntegrationRepository {
|
|||
createOrUpdateIntegration(
|
||||
org: string,
|
||||
name: string,
|
||||
picture: string,
|
||||
picture: string | undefined,
|
||||
type: 'article' | 'social',
|
||||
internalId: string,
|
||||
provider: string,
|
||||
|
|
@ -101,7 +131,7 @@ export class IntegrationRepository {
|
|||
providerIdentifier: provider,
|
||||
token,
|
||||
profile: username,
|
||||
picture,
|
||||
...(picture ? { picture } : {}),
|
||||
inBetweenSteps: isBetweenSteps,
|
||||
refreshToken,
|
||||
...(expiresIn
|
||||
|
|
@ -120,7 +150,7 @@ export class IntegrationRepository {
|
|||
inBetweenSteps: isBetweenSteps,
|
||||
}
|
||||
: {}),
|
||||
picture,
|
||||
...(picture ? { picture } : {}),
|
||||
profile: username,
|
||||
providerIdentifier: provider,
|
||||
token,
|
||||
|
|
@ -218,12 +248,79 @@ export class IntegrationRepository {
|
|||
return integration?.integration;
|
||||
}
|
||||
|
||||
async updateOnCustomerName(org: string, id: string, name: string) {
|
||||
const customer = !name
|
||||
? undefined
|
||||
: (await this._customers.model.customer.findFirst({
|
||||
where: {
|
||||
orgId: org,
|
||||
name,
|
||||
},
|
||||
})) ||
|
||||
(await this._customers.model.customer.create({
|
||||
data: {
|
||||
name,
|
||||
orgId: org,
|
||||
},
|
||||
}));
|
||||
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: org,
|
||||
},
|
||||
data: {
|
||||
customer: !customer
|
||||
? { disconnect: true }
|
||||
: {
|
||||
connect: {
|
||||
id: customer.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateIntegrationGroup(org: string, id: string, group: string) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: org,
|
||||
},
|
||||
data: !group
|
||||
? {
|
||||
customer: {
|
||||
disconnect: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
customer: {
|
||||
connect: {
|
||||
id: group,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
customers(orgId: string) {
|
||||
return this._customers.model.customer.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getIntegrationsList(org: string) {
|
||||
return this._integration.model.integration.findMany({
|
||||
where: {
|
||||
organizationId: org,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class IntegrationService {
|
|||
async createOrUpdateIntegration(
|
||||
org: string,
|
||||
name: string,
|
||||
picture: string,
|
||||
picture: string | undefined,
|
||||
type: 'article' | 'social',
|
||||
internalId: string,
|
||||
provider: string,
|
||||
|
|
@ -54,7 +54,9 @@ export class IntegrationService {
|
|||
timezone?: number,
|
||||
customInstanceDetails?: string
|
||||
) {
|
||||
const uploadedPicture = await this.storage.uploadSimple(picture);
|
||||
const uploadedPicture = picture
|
||||
? await this.storage.uploadSimple(picture)
|
||||
: undefined;
|
||||
return this._integrationRepository.createOrUpdateIntegration(
|
||||
org,
|
||||
name,
|
||||
|
|
@ -73,6 +75,14 @@ export class IntegrationService {
|
|||
);
|
||||
}
|
||||
|
||||
updateIntegrationGroup(org: string, id: string, group: string) {
|
||||
return this._integrationRepository.updateIntegrationGroup(org, id, group);
|
||||
}
|
||||
|
||||
updateOnCustomerName(org: string, id: string, name: string) {
|
||||
return this._integrationRepository.updateOnCustomerName(org, id, name);
|
||||
}
|
||||
|
||||
getIntegrationsList(org: string) {
|
||||
return this._integrationRepository.getIntegrationsList(org);
|
||||
}
|
||||
|
|
@ -153,7 +163,7 @@ export class IntegrationService {
|
|||
await this.createOrUpdateIntegration(
|
||||
integration.organizationId,
|
||||
integration.name,
|
||||
integration.picture!,
|
||||
undefined,
|
||||
'social',
|
||||
integration.internalId,
|
||||
integration.providerIdentifier,
|
||||
|
|
@ -378,6 +388,10 @@ export class IntegrationService {
|
|||
return [];
|
||||
}
|
||||
|
||||
customers(orgId: string) {
|
||||
return this._integrationRepository.customers(orgId);
|
||||
}
|
||||
|
||||
getPlugsByIntegrationId(org: string, integrationId: string) {
|
||||
return this._integrationRepository.getPlugsByIntegrationId(
|
||||
org,
|
||||
|
|
@ -385,56 +399,63 @@ export class IntegrationService {
|
|||
);
|
||||
}
|
||||
|
||||
processPlugs(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
delay: number,
|
||||
funcName: string
|
||||
) {
|
||||
return this._workerServiceProducer.emit('plugs', {
|
||||
id: integrationId + '-' + funcName,
|
||||
async processPlugs(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
const getPlugById = await this._integrationRepository.getPlug(data.plugId);
|
||||
if (!getPlugById) {
|
||||
return ;
|
||||
}
|
||||
|
||||
const integration = this._integrationManager.getSocialIntegration(
|
||||
getPlugById.integration.providerIdentifier
|
||||
);
|
||||
|
||||
const findPlug = this._integrationManager
|
||||
.getAllPlugs()
|
||||
.find(
|
||||
(p) => p.identifier === getPlugById.integration.providerIdentifier
|
||||
)!;
|
||||
|
||||
console.log(data.postId);
|
||||
|
||||
// @ts-ignore
|
||||
const process = await integration[getPlugById.plugFunction](
|
||||
getPlugById.integration,
|
||||
data.postId,
|
||||
JSON.parse(getPlugById.data).reduce((all: any, current: any) => {
|
||||
all[current.name] = current.value;
|
||||
return all;
|
||||
}, {})
|
||||
);
|
||||
|
||||
if (process) {
|
||||
return ;
|
||||
}
|
||||
|
||||
if (data.totalRuns === data.currentRun) {
|
||||
return ;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + data.postId + '_' + findPlug.identifier,
|
||||
options: {
|
||||
delay: 0, // delay,
|
||||
delay: 0, // runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
retry: 1,
|
||||
delay,
|
||||
orgId,
|
||||
integrationId: integrationId,
|
||||
funcName: funcName,
|
||||
plugId: data.plugId,
|
||||
postId: data.postId,
|
||||
delay: data.delay,
|
||||
totalRuns: data.totalRuns,
|
||||
currentRun: data.currentRun + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async activatedPlug(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
funcName: string,
|
||||
type: 'add' | 'remove'
|
||||
) {
|
||||
const loadIntegration = await this.getIntegrationById(orgId, integrationId);
|
||||
const allPlugs = this._integrationManager.getAllPlugs();
|
||||
const findPlug = allPlugs.find(
|
||||
(p) => p.identifier === loadIntegration?.providerIdentifier!
|
||||
)!;
|
||||
const plug = findPlug.plugs.find((p: any) => p.methodName === funcName)!;
|
||||
|
||||
if (type === 'add') {
|
||||
return this.processPlugs(
|
||||
orgId,
|
||||
integrationId,
|
||||
plug.runEveryMilliseconds,
|
||||
funcName
|
||||
);
|
||||
} else {
|
||||
console.log(integrationId + '-' + funcName);
|
||||
return this._workerServiceProducer.delete(
|
||||
'plugs',
|
||||
integrationId + '-' + funcName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async createOrUpdatePlug(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
|
|
@ -446,9 +467,9 @@ export class IntegrationService {
|
|||
body
|
||||
);
|
||||
|
||||
if (activated) {
|
||||
await this.activatedPlug(orgId, integrationId, body.func, 'add');
|
||||
}
|
||||
return {
|
||||
activated,
|
||||
};
|
||||
}
|
||||
|
||||
async changePlugActivation(orgId: string, plugId: string, status: boolean) {
|
||||
|
|
@ -459,58 +480,24 @@ export class IntegrationService {
|
|||
status
|
||||
);
|
||||
|
||||
if (status) {
|
||||
await this.activatedPlug(orgId, integrationId, plugFunction, 'add');
|
||||
} else {
|
||||
await this.activatedPlug(orgId, integrationId, plugFunction, 'remove');
|
||||
}
|
||||
|
||||
return { id };
|
||||
}
|
||||
|
||||
async loadExisingData (methodName: string, integrationId: string, id: string[]) {
|
||||
const exisingData = await this._integrationRepository.loadExisingData(methodName, integrationId, id);
|
||||
const loadOnlyIds = exisingData.map(p => p.value);
|
||||
async getPlugs(orgId: string, integrationId: string) {
|
||||
return this._integrationRepository.getPlugs(orgId, integrationId);
|
||||
}
|
||||
|
||||
async loadExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
id: string[]
|
||||
) {
|
||||
const exisingData = await this._integrationRepository.loadExisingData(
|
||||
methodName,
|
||||
integrationId,
|
||||
id
|
||||
);
|
||||
const loadOnlyIds = exisingData.map((p) => p.value);
|
||||
return difference(id, loadOnlyIds);
|
||||
}
|
||||
|
||||
async startPlug(data: {
|
||||
orgId: string;
|
||||
integrationId: string;
|
||||
funcName: string;
|
||||
retry: number;
|
||||
delay: number;
|
||||
}) {
|
||||
const integration = await this.getIntegrationById(
|
||||
data.orgId,
|
||||
data.integrationId
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const plugInformation = (
|
||||
await this._integrationRepository.getPlugsByIntegrationId(
|
||||
data.orgId,
|
||||
data.integrationId
|
||||
)
|
||||
).find((p) => p.plugFunction === data.funcName)!;
|
||||
|
||||
const plugData = JSON.parse(plugInformation.data).reduce(
|
||||
(all: any, current: any) => ({
|
||||
...all,
|
||||
[current.name]: current.value,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const integrationInstance = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const ids = await integrationInstance[data.funcName](integration, plugData, this.loadExisingData.bind(this));
|
||||
return this._integrationRepository.saveExisingData(data.funcName, data.integrationId, ids);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,8 +150,8 @@ export class OrganizationRepository {
|
|||
|
||||
if (
|
||||
process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
checkForSubscription?.subscription?.subscriptionTier !==
|
||||
SubscriptionTier.PRO
|
||||
checkForSubscription?.subscription?.subscriptionTier ===
|
||||
SubscriptionTier.STANDARD
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,13 @@ export class PostsService {
|
|||
true
|
||||
);
|
||||
|
||||
await this.checkPlugs(
|
||||
integration.organizationId,
|
||||
getIntegration.identifier,
|
||||
integration.id,
|
||||
publishedPosts[0].postId
|
||||
);
|
||||
|
||||
return {
|
||||
postId: publishedPosts[0].postId,
|
||||
releaseURL: publishedPosts[0].releaseURL,
|
||||
|
|
@ -312,6 +319,42 @@ export class PostsService {
|
|||
}
|
||||
}
|
||||
|
||||
private async checkPlugs(
|
||||
orgId: string,
|
||||
providerName: string,
|
||||
integrationId: string,
|
||||
postId: string
|
||||
) {
|
||||
const loadAllPlugs = this._integrationManager.getAllPlugs();
|
||||
const getPlugs = await this._integrationService.getPlugs(
|
||||
orgId,
|
||||
integrationId
|
||||
);
|
||||
|
||||
const currentPlug = loadAllPlugs.find((p) => p.identifier === providerName);
|
||||
|
||||
for (const plug of getPlugs) {
|
||||
const runPlug = currentPlug?.plugs?.find((p: any) => p.methodName === plug.plugFunction)!;
|
||||
if (!runPlug) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + postId + '_' + runPlug.identifier,
|
||||
options: {
|
||||
delay: 0, // runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
plugId: plug.id,
|
||||
postId,
|
||||
delay: 0, // runPlug.runEveryMilliseconds,
|
||||
totalRuns: runPlug.totalRuns,
|
||||
currentRun: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async postArticle(integration: Integration, posts: Post[]) {
|
||||
const getIntegration = this._integrationManager.getArticlesIntegration(
|
||||
integration.providerIdentifier
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ model Organization {
|
|||
usedCodes UsedCodes[]
|
||||
credits Credits[]
|
||||
plugs Plugs[]
|
||||
customers Customer[]
|
||||
}
|
||||
|
||||
model User {
|
||||
|
|
@ -242,6 +243,19 @@ model Subscription {
|
|||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
orgId String
|
||||
organization Organization @relation(fields: [orgId], references: [id])
|
||||
integrations Integration[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@unique([orgId, name, deletedAt])
|
||||
}
|
||||
|
||||
model Integration {
|
||||
id String @id @default(cuid())
|
||||
internalId String
|
||||
|
|
@ -265,6 +279,8 @@ model Integration {
|
|||
refreshNeeded Boolean @default(false)
|
||||
postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
|
||||
customInstanceDetails String?
|
||||
customerId String?
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
plugs Plugs[]
|
||||
exisingPlugData ExisingPlugData[]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ export class BillingSubscribeDto {
|
|||
|
||||
@IsIn(['STANDARD', 'PRO', 'TEAM', 'ULTIMATE'])
|
||||
billing: 'STANDARD' | 'PRO' | 'TEAM' | 'ULTIMATE';
|
||||
|
||||
utm: string;
|
||||
|
||||
tolt: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsDefined, IsIn, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class Collaborators {
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
label: string;
|
||||
}
|
||||
export class InstagramDto {
|
||||
@IsIn(['post', 'story'])
|
||||
@IsDefined()
|
||||
post_type: 'post' | 'story';
|
||||
|
||||
@Type(() => Collaborators)
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
collaborators: Collaborators[];
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class RefreshToken {
|
||||
constructor(
|
||||
public identifier: string,
|
||||
|
|
@ -18,7 +20,11 @@ export class NotEnoughScopes {
|
|||
}
|
||||
|
||||
export abstract class SocialAbstract {
|
||||
async fetch(url: string, options: RequestInit = {}, identifier = '') {
|
||||
async fetch(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
identifier = ''
|
||||
): Promise<Response> {
|
||||
const request = await fetch(url, options);
|
||||
|
||||
if (request.status === 200 || request.status === 201) {
|
||||
|
|
@ -33,7 +39,15 @@ export abstract class SocialAbstract {
|
|||
json = '{}';
|
||||
}
|
||||
|
||||
if (request.status === 401 || json.includes('OAuthException')) {
|
||||
if (json.includes('rate_limit_exceeded') || json.includes('Rate limit')) {
|
||||
await timer(2000);
|
||||
return this.fetch(url, options, identifier);
|
||||
}
|
||||
|
||||
if (
|
||||
request.status === 401 ||
|
||||
(json.includes('OAuthException') && !json.includes("Unsupported format") && !json.includes('2207018'))
|
||||
) {
|
||||
throw new RefreshToken(identifier, json, options.body!);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import dayjs from 'dayjs';
|
|||
import { Integration } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import sharp from 'sharp';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'bluesky';
|
||||
|
|
@ -116,7 +118,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let loadCid = '';
|
||||
let loadUri = '';
|
||||
const cidUrl = [] as { cid: string; url: string, rev: string }[];
|
||||
const cidUrl = [] as { cid: string; url: string; rev: string }[];
|
||||
for (const post of postDetails) {
|
||||
const images = await Promise.all(
|
||||
post.media?.map(async (p) => {
|
||||
|
|
@ -134,9 +136,9 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
const rt = new RichText({
|
||||
text: post.message,
|
||||
})
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent)
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
// @ts-ignore
|
||||
const { cid, uri, commit } = await agent.post({
|
||||
|
|
@ -179,9 +181,143 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
return postDetails.map((p, index) => ({
|
||||
id: p.id,
|
||||
postId: cidUrl[index].cid,
|
||||
postId: cidUrl[index].url,
|
||||
status: 'completed',
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url.split('/').pop()}`,
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url
|
||||
.split('/')
|
||||
.pop()}`,
|
||||
}));
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'bluesky-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const getThread = await agent.getPostThread({
|
||||
uri: id,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await agent.repost(
|
||||
// @ts-ignore
|
||||
getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
getThread.data.thread.post?.cid
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'bluesky-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const getThread = await agent.getPostThread({
|
||||
uri: id,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
const rt = new RichText({
|
||||
text: fields.post,
|
||||
});
|
||||
|
||||
await agent.post({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
reply: {
|
||||
root: {
|
||||
// @ts-ignore
|
||||
uri: getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
cid: getThread.data.thread.post?.cid,
|
||||
},
|
||||
parent: {
|
||||
// @ts-ignore
|
||||
uri: getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
cid: getThread.data.thread.post?.cid,
|
||||
},
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,8 +298,8 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
const until = dayjs().endOf('day').unix()
|
||||
const since = dayjs().subtract(date, 'day').unix();
|
||||
|
||||
const { data } = await (
|
||||
await this.fetch(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { string } from 'yup';
|
||||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
|
||||
export class InstagramProvider
|
||||
extends SocialAbstract
|
||||
|
|
@ -204,10 +204,11 @@ export class InstagramProvider
|
|||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
postDetails: PostDetails<InstagramDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...theRest] = postDetails;
|
||||
|
||||
console.log('in progress');
|
||||
const isStory = firstPost.settings.post_type === 'story';
|
||||
const medias = await Promise.all(
|
||||
firstPost?.media?.map(async (m) => {
|
||||
const caption =
|
||||
|
|
@ -219,18 +220,34 @@ export class InstagramProvider
|
|||
const mediaType =
|
||||
m.url.indexOf('.mp4') > -1
|
||||
? firstPost?.media?.length === 1
|
||||
? `video_url=${m.url}&media_type=REELS`
|
||||
? isStory
|
||||
? `video_url=${m.url}&media_type=STORIES`
|
||||
: `video_url=${m.url}&media_type=REELS`
|
||||
: isStory
|
||||
? `video_url=${m.url}&media_type=STORIES`
|
||||
: `video_url=${m.url}&media_type=VIDEO`
|
||||
: isStory
|
||||
? `image_url=${m.url}&media_type=STORIES`
|
||||
: `image_url=${m.url}`;
|
||||
console.log('in progress1');
|
||||
|
||||
const collaborators =
|
||||
firstPost?.settings?.collaborators?.length && !isStory
|
||||
? `&collaborators=${JSON.stringify(
|
||||
firstPost?.settings?.collaborators.map((p) => p.label)
|
||||
)}`
|
||||
: ``;
|
||||
|
||||
console.log(collaborators);
|
||||
const { id: photoId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}&access_token=${accessToken}${caption}`,
|
||||
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}${collaborators}&access_token=${accessToken}${caption}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
console.log('in progress2');
|
||||
|
||||
let status = 'IN_PROGRESS';
|
||||
while (status === 'IN_PROGRESS') {
|
||||
|
|
@ -242,6 +259,7 @@ export class InstagramProvider
|
|||
await timer(3000);
|
||||
status = status_code;
|
||||
}
|
||||
console.log('in progress3');
|
||||
|
||||
return photoId;
|
||||
}) || []
|
||||
|
|
@ -357,8 +375,8 @@ export class InstagramProvider
|
|||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
const until = dayjs().endOf('day').unix();
|
||||
const since = dayjs().subtract(date, 'day').unix();
|
||||
|
||||
const { data, ...all } = await (
|
||||
await fetch(
|
||||
|
|
@ -377,4 +395,12 @@ export class InstagramProvider
|
|||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
music(accessToken: string, data: { q: string }) {
|
||||
return this.fetch(
|
||||
`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(
|
||||
data.q
|
||||
)}&access_token=${accessToken}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/
|
|||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class LinkedinPageProvider
|
||||
extends LinkedinProvider
|
||||
|
|
@ -363,35 +364,32 @@ export class LinkedinPageProvider
|
|||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'linkedin-page-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 7200000,
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the post',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
fields: { likesAmount: number },
|
||||
loadExisingData: (
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
id: string[]
|
||||
) => Promise<string[]>
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
const all = await // const { elements } = await (
|
||||
(
|
||||
const {
|
||||
likesSummary: { totalLikes },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/rest/posts?author=${encodeURIComponent(
|
||||
`urn:li:organization:${integration.internalId}`
|
||||
)}&q=author&count=10&sortBy=LAST_MODIFIED`,
|
||||
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -404,82 +402,36 @@ export class LinkedinPageProvider
|
|||
)
|
||||
).json();
|
||||
|
||||
// only post published in the last week
|
||||
const lastWeekPosts = all.elements.filter((element: any) => {
|
||||
const postDate = new Date(element.publishedAt).getTime();
|
||||
const weekAgo = new Date().getTime() - 604800000;
|
||||
return postDate > weekAgo;
|
||||
});
|
||||
|
||||
const getLastFiveLikes = await Promise.all(
|
||||
lastWeekPosts.map(async (element: any) => {
|
||||
const {
|
||||
likesSummary: { totalLikes },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(
|
||||
element.id
|
||||
)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return { id: element.id, totalLikes };
|
||||
})
|
||||
);
|
||||
|
||||
const findLikes = getLastFiveLikes.filter(
|
||||
(element) => element.totalLikes >= fields.likesAmount
|
||||
);
|
||||
|
||||
if (findLikes.length === 0) {
|
||||
return [];
|
||||
if (totalLikes >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
||||
body: JSON.stringify({
|
||||
author: `urn:li:organization:${integration.internalId}`,
|
||||
commentary: '',
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
targetEntities: [],
|
||||
thirdPartyDistributionChannels: [],
|
||||
},
|
||||
lifecycleState: 'PUBLISHED',
|
||||
isReshareDisabledByAuthor: false,
|
||||
reshareContext: {
|
||||
parent: id,
|
||||
},
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const checkIfAlreadyPosted = await loadExisingData(
|
||||
'autoRepostPost',
|
||||
integration.id,
|
||||
findLikes.map((p) => p.id)
|
||||
);
|
||||
|
||||
if (checkIfAlreadyPosted.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
||||
body: JSON.stringify({
|
||||
author: `urn:li:organization:${integration.internalId}`,
|
||||
commentary: '',
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
targetEntities: [],
|
||||
thirdPartyDistributionChannels: [],
|
||||
},
|
||||
lifecycleState: 'PUBLISHED',
|
||||
isReshareDisabledByAuthor: false,
|
||||
reshareContext: {
|
||||
parent: checkIfAlreadyPosted[0],
|
||||
},
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return [checkIfAlreadyPosted[0]];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { Integration } from '@prisma/client';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { string } from 'yup';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
|
|
@ -158,7 +159,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
async company(token: string, data: { url: string }) {
|
||||
const { url } = data;
|
||||
const getCompanyVanity = url.match(
|
||||
/^https?:\/\/?www\.?linkedin\.com\/company\/([^/]+)\/$/
|
||||
/^https?:\/\/(?:www\.)?linkedin\.com\/company\/([^/]+)\/?$/
|
||||
);
|
||||
if (!getCompanyVanity || !getCompanyVanity?.length) {
|
||||
throw new Error('Invalid LinkedIn company URL');
|
||||
|
|
@ -284,6 +285,32 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
private fixText(text: string) {
|
||||
const pattern = /@\[.+?]\(urn:li:organization.+?\)/g;
|
||||
const matches = text.match(pattern) || [];
|
||||
const splitAll = text.split(pattern);
|
||||
const splitTextReformat = splitAll.map((p) => {
|
||||
return p
|
||||
.replace(/\*/g, '\\*')
|
||||
.replace(/\(/g, '\\(')
|
||||
.replace(/\)/g, '\\)')
|
||||
.replace(/\{/g, '\\{')
|
||||
.replace(/}/g, '\\}')
|
||||
.replace(/@/g, '\\@');
|
||||
});
|
||||
|
||||
const connectAll = splitTextReformat.reduce((all, current) => {
|
||||
const match = matches.shift();
|
||||
all.push(current);
|
||||
if (match) {
|
||||
all.push(match);
|
||||
}
|
||||
return all;
|
||||
}, [] as string[]);
|
||||
|
||||
return connectAll.join('');
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
|
|
@ -342,10 +369,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
type === 'personal'
|
||||
? `urn:li:person:${id}`
|
||||
: `urn:li:organization:${id}`,
|
||||
commentary: removeMarkdown({
|
||||
text: firstPost.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
|
||||
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
|
||||
}).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
|
||||
commentary: this.fixText(firstPost.message),
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
|
|
@ -410,12 +434,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
? `urn:li:person:${id}`
|
||||
: `urn:li:organization:${id}`,
|
||||
object: topPostId,
|
||||
message: {
|
||||
text: removeMarkdown({
|
||||
text: post.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
|
||||
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
|
||||
}).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
|
||||
},
|
||||
message: this.fixText(post.message),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_in: expiresIn,
|
||||
scope
|
||||
scope,
|
||||
} = await (
|
||||
await this.fetch('https://www.reddit.com/api/v1/access_token', {
|
||||
method: 'POST',
|
||||
|
|
@ -300,18 +300,28 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
)
|
||||
).json();
|
||||
|
||||
const newData = await (
|
||||
await this.fetch(
|
||||
`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
const newData = await new Promise<{id: string, name: string}[]>(async (res) => {
|
||||
try {
|
||||
const flair = await (
|
||||
await this.fetch(
|
||||
`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
res(flair);
|
||||
}
|
||||
catch (err) {
|
||||
return res([]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
subreddit: data.subreddit,
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
const until = dayjs().endOf('day').unix();
|
||||
const since = dayjs().subtract(date, 'day').unix();
|
||||
|
||||
const { data, ...all } = await (
|
||||
await fetch(
|
||||
|
|
@ -332,7 +332,6 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
)
|
||||
).json();
|
||||
|
||||
console.log(data);
|
||||
return (
|
||||
data?.map((d: any) => ({
|
||||
label: capitalize(d.name),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,username',
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,union_id,username',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -102,10 +102,11 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
code: params.code,
|
||||
grant_type: 'authorization_code',
|
||||
code_verifier: params.codeVerifier,
|
||||
redirect_uri:
|
||||
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
|
||||
? `https://integration.git.sn/integrations/social/tiktok`
|
||||
: `${process.env.FRONTEND_URL}/integrations/social/tiktok`,
|
||||
redirect_uri: `${
|
||||
process?.env?.FRONTEND_URL?.indexOf('https') === -1
|
||||
? 'https://redirectmeto.com/'
|
||||
: ''
|
||||
}${process?.env?.FRONTEND_URL}/integrations/social/tiktok`
|
||||
};
|
||||
|
||||
const { access_token, refresh_token, scope } = await (
|
||||
|
|
@ -118,6 +119,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
})
|
||||
).json();
|
||||
|
||||
console.log(this.scopes, scope);
|
||||
this.checkScopes(this.scopes, scope);
|
||||
|
||||
const {
|
||||
|
|
@ -222,58 +224,51 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
postDetails: PostDetails<TikTokDto>[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
try {
|
||||
const [firstPost, ...comments] = postDetails;
|
||||
const [firstPost, ...comments] = postDetails;
|
||||
|
||||
const {
|
||||
data: { publish_id },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_info: {
|
||||
title: firstPost.message,
|
||||
privacy_level: firstPost.settings.privacy_level,
|
||||
disable_duet: !firstPost.settings.duet,
|
||||
disable_comment: !firstPost.settings.comment,
|
||||
disable_stitch: !firstPost.settings.stitch,
|
||||
brand_content_toggle: firstPost.settings.brand_content_toggle,
|
||||
brand_organic_toggle: firstPost.settings.brand_organic_toggle,
|
||||
},
|
||||
source_info: {
|
||||
source: 'PULL_FROM_URL',
|
||||
video_url: firstPost?.media?.[0]?.url!,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { url, id: videoId } = await this.uploadedVideoSuccess(
|
||||
integration.profile!,
|
||||
publish_id,
|
||||
accessToken
|
||||
);
|
||||
|
||||
return [
|
||||
const {
|
||||
data: { publish_id },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/',
|
||||
{
|
||||
id: firstPost.id,
|
||||
releaseURL: url,
|
||||
postId: String(videoId),
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
throw new BadBody('titok-error', JSON.stringify(err), {
|
||||
// @ts-ignore
|
||||
postDetails,
|
||||
});
|
||||
}
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_info: {
|
||||
title: firstPost.message,
|
||||
privacy_level: firstPost.settings.privacy_level,
|
||||
disable_duet: !firstPost.settings.duet,
|
||||
disable_comment: !firstPost.settings.comment,
|
||||
disable_stitch: !firstPost.settings.stitch,
|
||||
brand_content_toggle: firstPost.settings.brand_content_toggle,
|
||||
brand_organic_toggle: firstPost.settings.brand_organic_toggle,
|
||||
},
|
||||
source_info: {
|
||||
source: 'PULL_FROM_URL',
|
||||
video_url: firstPost?.media?.[0]?.url!,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { url, id: videoId } = await this.uploadedVideoSuccess(
|
||||
integration.profile!,
|
||||
publish_id,
|
||||
accessToken
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
releaseURL: url,
|
||||
postId: String(videoId),
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
|
|||
import removeMd from 'remove-markdown';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { string } from 'yup';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class XProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'x';
|
||||
|
|
@ -20,10 +20,112 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [];
|
||||
|
||||
@Plug({
|
||||
identifier: 'x-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
if (
|
||||
(await client.v2.tweetLikedBy(id)).meta.result_count >=
|
||||
+fields.likesAmount
|
||||
) {
|
||||
await timer(2000);
|
||||
await client.v2.retweet(integration.internalId, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'x-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
if (
|
||||
(await client.v2.tweetLikedBy(id)).meta.result_count >=
|
||||
+fields.likesAmount
|
||||
) {
|
||||
await timer(2000);
|
||||
|
||||
await client.v2.tweet({
|
||||
text: removeMd(fields.post.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')).replace(
|
||||
'𝔫𝔢𝔴𝔩𝔦𝔫𝔢',
|
||||
'\n'
|
||||
),
|
||||
reply: { in_reply_to_tweet_id: id },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const startingClient = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
});
|
||||
const {
|
||||
accessToken,
|
||||
|
|
|
|||
|
|
@ -270,12 +270,13 @@ export class StripeService {
|
|||
body: BillingSubscribeDto,
|
||||
price: string
|
||||
) {
|
||||
const isUtm = body.utm ? `&utm_source=${body.utm}` : '';
|
||||
const { url } = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
cancel_url: process.env['FRONTEND_URL'] + `/billing`,
|
||||
cancel_url: process.env['FRONTEND_URL'] + `/billing?cancel=true${isUtm}`,
|
||||
success_url:
|
||||
process.env['FRONTEND_URL'] +
|
||||
`/launches?onboarding=true&check=${uniqueId}`,
|
||||
`/launches?onboarding=true&check=${uniqueId}${isUtm}`,
|
||||
mode: 'subscription',
|
||||
subscription_data: {
|
||||
trial_period_days: 7,
|
||||
|
|
@ -285,6 +286,11 @@ export class StripeService {
|
|||
uniqueId,
|
||||
},
|
||||
},
|
||||
...body.tolt ? {
|
||||
metadata: {
|
||||
tolt_referral: body.tolt,
|
||||
}
|
||||
} : {},
|
||||
allow_promotion_codes: true,
|
||||
line_items: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import {
|
|||
} from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
const { CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ACCESS_KEY, CLOUDFLARE_SECRET_ACCESS_KEY, CLOUDFLARE_BUCKETNAME, CLOUDFLARE_BUCKET_URL } =
|
||||
process.env;
|
||||
|
|
@ -16,12 +19,16 @@ const R2 = new S3Client({
|
|||
},
|
||||
});
|
||||
|
||||
// Function to generate a random string
|
||||
function generateRandomString() {
|
||||
return makeId(20);
|
||||
}
|
||||
|
||||
export default async function handleR2Upload(
|
||||
endpoint: string,
|
||||
req: Request,
|
||||
res: Response
|
||||
) {
|
||||
|
||||
switch (endpoint) {
|
||||
case 'create-multipart-upload':
|
||||
return createMultipartUpload(req, res);
|
||||
|
|
@ -39,10 +46,13 @@ export default async function handleR2Upload(
|
|||
return res.status(404).end();
|
||||
}
|
||||
|
||||
export async function simpleUpload(data: Buffer, key: string, contentType: string) {
|
||||
export async function simpleUpload(data: Buffer, originalFilename: string, contentType: string) {
|
||||
const fileExtension = path.extname(originalFilename); // Extract extension
|
||||
const randomFilename = generateRandomString() + fileExtension; // Append extension
|
||||
|
||||
const params = {
|
||||
Bucket: CLOUDFLARE_BUCKETNAME,
|
||||
Key: key,
|
||||
Key: randomFilename,
|
||||
Body: data,
|
||||
ContentType: contentType,
|
||||
};
|
||||
|
|
@ -50,7 +60,7 @@ export async function simpleUpload(data: Buffer, key: string, contentType: strin
|
|||
const command = new PutObjectCommand({ ...params });
|
||||
await R2.send(command);
|
||||
|
||||
return CLOUDFLARE_BUCKET_URL + '/' + key;
|
||||
return CLOUDFLARE_BUCKET_URL + '/' + randomFilename;
|
||||
}
|
||||
|
||||
export async function createMultipartUpload(
|
||||
|
|
@ -58,11 +68,13 @@ export async function createMultipartUpload(
|
|||
res: Response
|
||||
) {
|
||||
const { file, fileHash, contentType } = req.body;
|
||||
const filename = file.name;
|
||||
const fileExtension = path.extname(file.name); // Extract extension
|
||||
const randomFilename = generateRandomString() + fileExtension; // Append extension
|
||||
|
||||
try {
|
||||
const params = {
|
||||
Bucket: CLOUDFLARE_BUCKETNAME,
|
||||
Key: `resources/${fileHash}/${filename}`,
|
||||
Key: `${randomFilename}`,
|
||||
ContentType: contentType,
|
||||
Metadata: {
|
||||
'x-amz-meta-file-hash': fileHash,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface VariableContextInterface {
|
|||
backendUrl: string;
|
||||
discordUrl: string;
|
||||
uploadDirectory: string;
|
||||
tolt: string;
|
||||
}
|
||||
const VariableContext = createContext({
|
||||
billingEnabled: false,
|
||||
|
|
@ -21,6 +22,7 @@ const VariableContext = createContext({
|
|||
backendUrl: '',
|
||||
discordUrl: '',
|
||||
uploadDirectory: '',
|
||||
tolt: '',
|
||||
} as VariableContextInterface);
|
||||
|
||||
export const VariableContextComponent: FC<
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -36,10 +36,10 @@
|
|||
"@aws-sdk/client-s3": "^3.410.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@copilotkit/react-core": "1.1.0",
|
||||
"@copilotkit/react-textarea": "1.1.0",
|
||||
"@copilotkit/react-ui": "1.1.0",
|
||||
"@copilotkit/runtime": "1.1.0",
|
||||
"@copilotkit/react-core": "^1.3.15",
|
||||
"@copilotkit/react-textarea": "^1.3.15",
|
||||
"@copilotkit/react-ui": "^1.3.15",
|
||||
"@copilotkit/runtime": "^1.3.15",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@mantine/core": "^5.10.5",
|
||||
"@mantine/dates": "^5.10.5",
|
||||
|
|
@ -148,6 +148,7 @@
|
|||
"tldts": "^6.1.47",
|
||||
"tslib": "^2.3.0",
|
||||
"twitter-api-v2": "^1.16.0",
|
||||
"twitter-text": "^3.1.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"utf-8-validate": "^5.0.10",
|
||||
"uuid": "^10.0.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue