feat: timing

This commit is contained in:
Nevo David 2024-09-20 16:28:47 +07:00
parent 6de19e4a8b
commit 3a785e1144
20 changed files with 801 additions and 177 deletions

View File

@ -26,6 +26,7 @@ import { ApiTags } from '@nestjs/swagger';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
@ApiTags('Integrations')
@Controller('/integrations')
@ -55,6 +56,7 @@ export class IntegrationsController {
inBetweenSteps: p.inBetweenSteps,
refreshNeeded: p.refreshNeeded,
type: p.type,
time: JSON.parse(p.postingTimes)
})),
};
}
@ -97,6 +99,14 @@ export class IntegrationsController {
return { url };
}
@Post('/:id/time')
async setTime(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string,
@Body() body: IntegrationTimeDto
) {
return this._integrationService.setTimes(org.id, id, body);
}
@Post('/function')
async functionIntegration(
@GetOrgFromRequest() org: Organization,
@ -238,7 +248,8 @@ export class IntegrationsController {
expiresIn,
username,
integrationProvider.isBetweenSteps,
body.refresh
body.refresh,
+body.timezone
);
}

View File

@ -13,7 +13,6 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
import { Organization, User } from '@prisma/client';
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {
@ -30,7 +29,6 @@ import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generato
export class PostsController {
constructor(
private _postsService: PostsService,
private _commentsService: CommentsService,
private _starsService: StarsService,
private _messagesService: MessagesService
) {}

View File

@ -0,0 +1,40 @@
import { HttpStatusCode } from 'axios';
export const dynamic = 'force-dynamic';
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
import { redirect } from 'next/navigation';
export default async function Page({
params: { provider },
searchParams,
}: {
params: { provider: string };
searchParams: any;
}) {
if (provider === 'x') {
searchParams = {
...searchParams,
state: searchParams.oauth_token || '',
code: searchParams.oauth_verifier || '',
refresh: searchParams.refresh || '',
};
}
const data = await internalFetch(`/integrations/social/${provider}/connect`, {
method: 'POST',
body: JSON.stringify(searchParams),
});
if (data.status === HttpStatusCode.NotAcceptable) {
return redirect(`/launches?scope=missing`);
}
const { inBetweenSteps, id } = await data.json();
if (inBetweenSteps && !searchParams.refresh) {
return redirect(`/launches?added=${provider}&continue=${id}`);
}
return redirect(`/launches?added=${provider}`);
}

View File

@ -1,10 +1,7 @@
import { HttpStatusCode } from 'axios';
import { IntegrationRedirectComponent } from '@gitroom/frontend/components/launches/integration.redirect.component';
export const dynamic = 'force-dynamic';
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
import { redirect } from 'next/navigation';
export default async function Page({
params: { provider },
searchParams,
@ -12,29 +9,5 @@ export default async function Page({
params: { provider: string };
searchParams: any;
}) {
if (provider === 'x') {
searchParams = {
...searchParams,
state: searchParams.oauth_token || '',
code: searchParams.oauth_verifier || '',
refresh: searchParams.refresh || '',
};
}
const data = await internalFetch(`/integrations/social/${provider}/connect`, {
method: 'POST',
body: JSON.stringify(searchParams),
});
if (data.status === HttpStatusCode.NotAcceptable) {
return redirect(`/launches?scope=missing`);
}
const { inBetweenSteps, id } = await data.json();
if (inBetweenSteps && !searchParams.refresh) {
return redirect(`/launches?added=${provider}&continue=${id}`);
}
return redirect(`/launches?added=${provider}`);
return <IntegrationRedirectComponent />;
}

View File

@ -0,0 +1,13 @@
import { ReactNode } from 'react';
export default async function IntegrationLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div className="text-6xl text-center mt-[50px]">
Adding channel, Redirecting You{children}
</div>
);
}

View File

@ -48,6 +48,7 @@ import { useStateCallback } from '@gitroom/react/helpers/use.state.callback';
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';
export const AddEditModal: FC<{
date: dayjs.Dayjs;
@ -111,6 +112,8 @@ export const AddEditModal: FC<{
setSelectedIntegrations([
integrations.find((p) => p.id === existingData.integration)!,
]);
} else if (integrations.length === 1) {
setSelectedIntegrations([integrations[0]]);
}
}, [existingData.integration]);
@ -279,7 +282,8 @@ export const AddEditModal: FC<{
) {
if (
!(await deleteDialog(
`${key?.integration?.name} post is too long, it will be cropped, do you want to continue?`, 'Yes, continue'
`${key?.integration?.name} post is too long, it will be cropped, do you want to continue?`,
'Yes, continue'
))
) {
await key.trigger();
@ -381,7 +385,9 @@ export const AddEditModal: FC<{
instructions="You are an assistant that help the user to schedule their social media posts, everytime somebody write something, try to use a function call, if not prompt the user that the request is invalid and you are here to assists with social media posts"
/>
)}
<div className={clsx('flex p-[10px] rounded-[4px] bg-primary gap-[20px]')}>
<div
className={clsx('flex p-[10px] rounded-[4px] bg-primary gap-[20px]')}
>
<div
className={clsx(
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
@ -402,7 +408,7 @@ export const AddEditModal: FC<{
</div>
</TopTitle>
{!existingData.integration && (
{!existingData.integration && integrations.length > 1 ? (
<PickPlatforms
integrations={integrations.filter((f) => !f.disabled)}
selectedIntegrations={[]}
@ -410,6 +416,35 @@ export const AddEditModal: FC<{
onChange={setSelectedIntegrations}
isMain={true}
/>
) : (
<div
className={clsx(
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
)}
>
<Image
src={selectedIntegrations?.[0]?.picture}
className="rounded-full"
alt={selectedIntegrations?.[0]?.identifier}
width={32}
height={32}
/>
{selectedIntegrations?.[0]?.identifier === 'youtube' ? (
<img
src="/icons/platforms/youtube.svg"
className="absolute z-10 -bottom-[5px] -right-[5px]"
width={20}
/>
) : (
<Image
src={`/icons/platforms/${selectedIntegrations?.[0]?.identifier}.png`}
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
alt={selectedIntegrations?.[0]?.identifier}
width={20}
height={20}
/>
)}
</div>
)}
<div
id="renderEditor"
@ -426,13 +461,15 @@ export const AddEditModal: FC<{
<Editor
order={index}
height={value.length > 1 ? 150 : 250}
commands={[
// ...commands
// .getCommands()
// .filter((f) => f.name === 'image'),
// newImage,
// postSelector(dateState),
]}
commands={
[
// ...commands
// .getCommands()
// .filter((f) => f.name === 'image'),
// newImage,
// postSelector(dateState),
]
}
value={p.content}
preview="edit"
// @ts-ignore

View File

@ -22,7 +22,8 @@ import weekOfYear from 'dayjs/plugin/weekOfYear';
dayjs.extend(isoWeek);
dayjs.extend(weekOfYear);
const CalendarContext = createContext({
export const CalendarContext = createContext({
currentDay: dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6,
currentWeek: dayjs().week(),
currentYear: dayjs().year(),
currentMonth: dayjs().month(),
@ -30,13 +31,16 @@ const CalendarContext = createContext({
integrations: [] as Integrations[],
trendings: [] as string[],
posts: [] as Array<Post & { integration: Integration }>,
reloadCalendarView: () => {/** empty **/},
reloadCalendarView: () => {
/** empty **/
},
display: 'week',
setFilters: (filters: {
currentWeek: number;
currentYear: number;
currentDay: 0 | 1 | 2 | 3 | 4 | 5 | 6;
currentMonth: number;
display: 'week' | 'month';
display: 'week' | 'month' | 'day';
}) => {
/** empty **/
},
@ -53,6 +57,7 @@ export interface Integrations {
identifier: string;
type: string;
picture: string;
time: { time: number }[];
}
function getWeekNumber(date: Date) {
@ -82,39 +87,37 @@ export const CalendarWeekProvider: FC<{
const [trendings] = useState<string[]>([]);
const searchParams = useSearchParams();
const display = searchParams.get('month') ? 'month' : 'week';
const display = searchParams.get('display') || 'week';
const [filters, setFilters] = useState({
currentWeek:
display === 'week'
? +(searchParams.get('week') || getWeekNumber(new Date()))
: 0,
currentMonth:
display === 'week' ? 0 : +(searchParams.get('month') || dayjs().month()),
currentDay: +(searchParams.get('day') || dayjs().day()) as
| 0
| 1
| 2
| 3
| 4
| 5
| 6,
currentWeek: +(searchParams.get('week') || getWeekNumber(new Date())),
currentMonth: +(searchParams.get('month') || dayjs().month()),
currentYear: +(searchParams.get('year') || dayjs().year()),
display,
});
const params = useMemo(() => {
return new URLSearchParams(
filters.currentWeek
? {
week: filters.currentWeek.toString(),
year: filters.currentYear.toString(),
}
: {
year: filters.currentYear.toString(),
month: (filters.currentMonth + 1).toString(),
}
).toString();
}, [filters]);
return new URLSearchParams({
display: filters.display,
day: filters.currentDay.toString(),
week: filters.currentWeek.toString(),
month: (filters.currentMonth + 1).toString(),
year: filters.currentYear.toString(),
}).toString();
}, [filters, display]);
const loadData = useCallback(
async () => {
const data = (await fetch(`/posts?${params}`)).json();
return data;
},
[filters, params]
);
const loadData = useCallback(async () => {
const data = (await fetch(`/posts?${params}`)).json();
return data;
}, [filters, params]);
const swr = useSWR(`/posts-${params}`, loadData, {
refreshInterval: 3600000,
@ -125,22 +128,23 @@ export const CalendarWeekProvider: FC<{
const setFiltersWrapper = useCallback(
(filters: {
currentDay: 0 | 1 | 2 | 3 | 4 | 5 | 6;
currentWeek: number;
currentYear: number;
currentMonth: number;
display: 'week' | 'month';
display: 'week' | 'month' | 'day';
}) => {
setFilters(filters);
setInternalData([]);
window.history.replaceState(
null,
'',
`/launches?${
filters.currentWeek
? `week=${filters.currentWeek}`
: `month=${filters.currentMonth}`
}&year=${filters.currentYear}`
);
const path = [
`day=${filters.currentDay}`,
`week=${filters.currentWeek}`,
`month=${filters.currentMonth}`,
`year=${filters.currentYear}`,
`display=${filters.display}`,
].filter((f) => f);
window.history.replaceState(null, '', `/launches?${path.join('&')}`);
},
[filters, swr.mutate]
);

View File

@ -1,14 +1,8 @@
'use client';
import React, {
FC,
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { FC, Fragment, useCallback, useMemo } from 'react';
import {
CalendarContext,
Integrations,
useCalendar,
} from '@gitroom/frontend/components/launches/calendar.context';
@ -30,6 +24,8 @@ import { IntegrationContext } from '@gitroom/frontend/components/launches/helper
import { PreviewPopup } from '@gitroom/frontend/components/marketplace/special.message';
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);
@ -44,6 +40,86 @@ export const days = [
];
export const hours = Array.from({ length: 24 }, (_, i) => i);
export const DayView = () => {
const calendar = useCalendar();
const { integrations, posts, currentYear, currentDay, currentWeek } =
calendar;
const options = useMemo(() => {
const createdPosts = posts.map((post) => ({
integration: [integrations.find((i) => i.id === post.integration.id)!],
image: post.integration.picture,
identifier: post.integration.providerIdentifier,
id: post.integration.id,
name: post.integration.name,
time: dayjs
.utc(post.publishDate)
.diff(dayjs.utc(post.publishDate).startOf('day'), 'minute'),
}));
return sortBy(
Object.values(
groupBy(
[
...createdPosts,
...integrations.flatMap((p) =>
p.time.flatMap((t) => ({
integration: p,
identifier: p.identifier,
name: p.name,
id: p.id,
image: p.picture,
time: t.time,
}))
),
],
(p: any) => p.time
)
),
(p) => p[0].time
);
}, [integrations, posts]);
return (
<div className="flex flex-col gap-[10px]">
{options.map((option) => (
<Fragment key={option[0].time}>
<div className="text-center text-[20px]">
{dayjs()
.utc()
.startOf('day')
.add(option[0].time, 'minute')
.local()
.format('HH:mm')}
</div>
<div
key={option[0].time}
className="bg-secondary min-h-[60px] border-[2px] border-secondary rounded-[10px] flex justify-center items-center gap-[10px] mb-[20px]"
>
<CalendarContext.Provider
value={{
...calendar,
integrations: option.flatMap((p) => p.integration),
}}
>
<CalendarColumn
getDate={dayjs()
.utc()
.year(currentYear)
.week(currentWeek)
.day(currentDay)
.startOf('day')
.add(option[0].time, 'minute')
.local()}
/>
</CalendarContext.Provider>
</div>
</Fragment>
))}
</div>
);
};
export const WeekView = () => {
const { currentYear, currentWeek } = useCalendar();
@ -155,7 +231,13 @@ export const Calendar = () => {
return (
<DNDProvider>
{display === 'week' ? <WeekView /> : <MonthView />}
{display === 'day' ? (
<DayView />
) : display === 'week' ? (
<WeekView />
) : (
<MonthView />
)}
</DNDProvider>
);
};
@ -183,7 +265,10 @@ export const CalendarColumn: FC<{
return posts.filter((post) => {
const pList = dayjs.utc(post.publishDate).local();
const check =
display === 'week'
display === 'day'
? pList.format('YYYY-MM-DD HH:mm') ===
getDate.format('YYYY-MM-DD HH:mm')
: display === 'week'
? pList.isSameOrAfter(getDate.startOf('hour')) &&
pList.isBefore(getDate.endOf('hour'))
: pList.format('DD/MM/YYYY') === getDate.format('DD/MM/YYYY');
@ -373,6 +458,7 @@ export const CalendarColumn: FC<{
>
<div className="relative w-full flex flex-col items-center p-[2.5px]">
<CalendarItem
display={display as 'day' | 'week' | 'month'}
isBeforeNow={isBeforeNow}
date={getDate}
state={post.state}
@ -384,14 +470,16 @@ export const CalendarColumn: FC<{
</div>
))}
</div>
{!isBeforeNow && (
{(display === 'day'
? !isBeforeNow && postList.length === 0
: !isBeforeNow) && (
<div
className="pb-[2.5px] px-[5px] flex-1 flex"
onClick={integrations.length ? addModal : addProvider}
>
<div
className={clsx(
display === 'month'
display === ('month' as any)
? 'flex-1 min-h-[40px] w-full'
: !postList.length
? 'h-full w-full absolute left-0 top-0 p-[5px]'
@ -399,11 +487,51 @@ export const CalendarColumn: FC<{
'flex items-center justify-center cursor-pointer pb-[2.5px]'
)}
>
<div
className={clsx(
'hover:before:content-["+"] w-full h-full text-seventh rounded-[10px] hover:border hover:border-seventh flex justify-center items-center'
)}
/>
{display !== 'day' && (
<div
className={clsx(
'hover:before:content-["+"] w-full h-full text-seventh rounded-[10px] hover:border hover:border-seventh flex justify-center items-center'
)}
/>
)}
{display === 'day' && (
<div
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={clsx(
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500'
)}
>
<Image
src={selectedIntegrations.picture}
className="rounded-full"
alt={selectedIntegrations.identifier}
width={32}
height={32}
/>
{selectedIntegrations.identifier === 'youtube' ? (
<img
src="/icons/platforms/youtube.svg"
className="absolute z-10 -bottom-[5px] -right-[5px]"
width={20}
/>
) : (
<Image
src={`/icons/platforms/${selectedIntegrations.identifier}.png`}
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
alt={selectedIntegrations.identifier}
width={20}
height={20}
/>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
@ -418,9 +546,10 @@ const CalendarItem: FC<{
editPost: () => void;
integrations: Integrations[];
state: State;
display: 'day' | 'week' | 'month';
post: Post & { integration: Integration };
}> = (props) => {
const { editPost, post, date, isBeforeNow, state } = props;
const { editPost, post, date, isBeforeNow, state, display } = props;
const [{ opacity }, dragRef] = useDrag(
() => ({
type: 'post',
@ -443,13 +572,18 @@ const CalendarItem: FC<{
)}
style={{ opacity }}
>
<div className="relative min-w-[20px] h-[20px]">
<div
className={clsx(
'relative min-w-[20px] h-[20px]',
display === 'day' ? 'h-[40px]' : 'h-[20px]'
)}
>
<img
className="w-[20px] h-[20px] rounded-full"
src={post.integration.picture!}
/>
<img
className="w-[12px] h-[12px] rounded-full absolute z-10 bottom-[0] right-0 border border-fifth"
className="w-[12px] h-[12px] rounded-full absolute z-10 top-[10px] right-0 border border-fifth"
src={`/icons/platforms/${post.integration?.providerIdentifier}.png`}
/>
</div>

View File

@ -7,7 +7,13 @@ import { useCallback } from 'react';
export const Filters = () => {
const week = useCalendar();
const betweenDates =
week.display === 'week'
week.display === 'day'
? dayjs()
.year(week.currentYear)
.isoWeek(week.currentWeek)
.day(week.currentDay)
.format('DD/MM/YYYY')
: week.display === 'week'
? dayjs()
.year(week.currentYear)
.isoWeek(week.currentWeek)
@ -31,76 +37,113 @@ export const Filters = () => {
.endOf('month')
.format('DD/MM/YYYY');
const setWeek = useCallback(() => {
const setDay = useCallback(() => {
week.setFilters({
currentDay: +dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6,
currentWeek: dayjs().isoWeek(),
currentYear: dayjs().year(),
currentMonth: 0,
currentMonth: dayjs().month(),
display: 'day',
});
}, [week]);
const setWeek = useCallback(() => {
week.setFilters({
currentDay: +dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6,
currentWeek: dayjs().isoWeek(),
currentYear: dayjs().year(),
currentMonth: dayjs().month(),
display: 'week',
});
}, [week]);
const setMonth = useCallback(() => {
week.setFilters({
currentDay: +dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6,
currentMonth: dayjs().month(),
currentWeek: 0,
currentWeek: dayjs().isoWeek(),
currentYear: dayjs().year(),
display: 'month',
});
}, [week]);
const next = useCallback(() => {
const increaseDay = week.display === 'day';
const increaseWeek =
week.display === 'week' ||
(week.display === 'day' && week.currentDay === 6);
const increaseMonth =
week.display === 'month' || (increaseWeek && week.currentWeek === 52);
week.setFilters({
currentWeek:
week.display === 'week'
? week.currentWeek === 52
? 1
: week.currentWeek + 1
: 0,
currentYear:
week.display === 'week'
? week.currentWeek === 52
? week.currentYear + 1
: week.currentYear
: week.currentMonth === 11
? week.currentYear + 1
: week.currentYear,
currentDay: (!increaseDay
? 0
: week.currentDay === 6
? 0
: week.currentDay + 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6,
currentWeek: !increaseWeek
? week.currentWeek
: week.currentWeek === 52
? 1
: week.currentWeek + 1,
currentYear: !increaseMonth
? week.currentYear
: week.currentMonth === 11
? week.currentYear + 1
: week.currentYear,
display: week.display as any,
currentMonth:
week.display === 'week'
? 0
: week.currentMonth === 11
? 0
: week.currentMonth + 1,
currentMonth: !increaseMonth
? week.currentMonth
: week.currentMonth === 11
? 0
: week.currentMonth + 1,
});
}, [week.display, week.currentMonth, week.currentWeek, week.currentYear]);
}, [
week.display,
week.currentMonth,
week.currentWeek,
week.currentYear,
week.currentDay,
]);
const previous = useCallback(() => {
week.setFilters({
currentWeek:
week.display === 'week'
? week.currentWeek === 1
? 52
: week.currentWeek - 1
: 0,
currentYear:
week.display === 'week'
? week.currentWeek === 1
? week.currentYear - 1
: week.currentYear
: week.currentMonth === 0
? week.currentYear - 1
: week.currentYear,
display: week.display as any,
currentMonth:
week.display === 'week'
? 0
: week.currentMonth === 0
? 11
: week.currentMonth - 1,
});
}, [week.display, week.currentMonth, week.currentWeek, week.currentYear]);
const decreaseDay = week.display === 'day';
const decreaseWeek =
week.display === 'week' ||
(week.display === 'day' && week.currentDay === 0);
const decreaseMonth =
week.display === 'month' || (decreaseWeek && week.currentWeek === 1);
week.setFilters({
currentDay: (!decreaseDay
? 0
: week.currentDay === 0
? 6
: week.currentDay - 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6,
currentWeek: !decreaseWeek
? week.currentWeek
: week.currentWeek === 1
? 52
: week.currentWeek - 1,
currentYear: !decreaseMonth
? week.currentYear
: week.currentMonth === 0
? week.currentYear - 1
: week.currentYear,
display: week.display as any,
currentMonth: !decreaseMonth
? week.currentMonth
: week.currentMonth === 0
? 11
: week.currentMonth - 1,
});
}, [
week.display,
week.currentMonth,
week.currentWeek,
week.currentYear,
week.currentDay,
]);
return (
<div className="text-textColor flex gap-[8px] items-center select-none">
<div onClick={previous}>
@ -118,7 +161,13 @@ export const Filters = () => {
</svg>
</div>
<div className="w-[80px] text-center">
{week.display === 'week'
{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>
@ -137,6 +186,15 @@ export const Filters = () => {
</svg>
</div>
<div className="flex-1">{betweenDates}</div>
<div
className={clsx(
'border border-tableBorder p-[10px]',
week.display === 'day' && 'bg-tableBorder'
)}
onClick={setDay}
>
Day
</div>
<div
className={clsx(
'border border-tableBorder p-[10px]',

View File

@ -0,0 +1,23 @@
'use client';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
dayjs.extend(utc);
dayjs.extend(timezone);
export const IntegrationRedirectComponent = () => {
const offset = dayjs.tz().utcOffset();
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const newUrl = `${pathname}/continue?${searchParams.toString()}&timezone=${offset}`;
useEffect(() => {
router.push(newUrl);
}, [newUrl]);
return null;
};

View File

@ -196,6 +196,7 @@ export const LaunchesComponent = () => {
{integration.name}
</div>
<Menu
mutate={mutate}
onChange={update}
id={integration.id}
canEnable={

View File

@ -4,16 +4,24 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useToaster } from '@gitroom/react/toaster/toaster';
import interClass from '@gitroom/react/helpers/inter.font';
import { useModals } from '@mantine/modals';
import { TimeTable } from '@gitroom/frontend/components/launches/time.table';
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload';
export const Menu: FC<{
canEnable: boolean;
canDisable: boolean;
id: string;
mutate: () => void,
onChange: (shouldReload: boolean) => void;
}> = (props) => {
const { canEnable, canDisable, id, onChange } = props;
const { canEnable, canDisable, id, onChange, mutate } = props;
const fetch = useFetch();
const { integrations } = useCalendar();
const toast = useToaster();
usePreventWindowUnload(true);
const modal = useModals();
const [show, setShow] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => {
setShow(false);
@ -80,6 +88,25 @@ export const Menu: FC<{
onChange(false);
}, []);
const editTimeTable = 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: false,
closeOnClickOutside: false,
children: (
<TimeTable integration={findIntegration!} mutate={mutate} />
),
});
setShow(false);
}, [integrations]);
return (
<div
className="cursor-pointer relative select-none"
@ -103,6 +130,23 @@ export const Menu: FC<{
onClick={(e) => e.stopPropagation()}
className={`absolute top-[100%] left-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder ${interClass} text-nowrap`}
>
<div className="flex gap-[12px] items-center" onClick={editTimeTable}>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 32 32"
fill="none"
>
<path
d="M16 5C13.6266 5 11.3066 5.70379 9.33316 7.02236C7.35977 8.34094 5.8217 10.2151 4.91345 12.4078C4.0052 14.6005 3.76756 17.0133 4.23058 19.3411C4.6936 21.6689 5.83649 23.8071 7.51472 25.4853C9.19295 27.1635 11.3312 28.3064 13.6589 28.7694C15.9867 29.2324 18.3995 28.9948 20.5922 28.0866C22.7849 27.1783 24.6591 25.6402 25.9776 23.6668C27.2962 21.6935 28 19.3734 28 17C27.9964 13.8185 26.7309 10.7684 24.4813 8.51874C22.2316 6.26909 19.1815 5.00364 16 5ZM16 27C14.0222 27 12.0888 26.4135 10.4443 25.3147C8.79981 24.2159 7.51809 22.6541 6.76121 20.8268C6.00433 18.9996 5.8063 16.9889 6.19215 15.0491C6.578 13.1093 7.53041 11.3275 8.92894 9.92893C10.3275 8.53041 12.1093 7.578 14.0491 7.19215C15.9889 6.80629 17.9996 7.00433 19.8268 7.7612C21.6541 8.51808 23.2159 9.79981 24.3147 11.4443C25.4135 13.0888 26 15.0222 26 17C25.997 19.6513 24.9425 22.1931 23.0678 24.0678C21.1931 25.9425 18.6513 26.997 16 27ZM21.7075 11.2925C21.8005 11.3854 21.8742 11.4957 21.9246 11.6171C21.9749 11.7385 22.0008 11.8686 22.0008 12C22.0008 12.1314 21.9749 12.2615 21.9246 12.3829C21.8742 12.5043 21.8005 12.6146 21.7075 12.7075L16.7075 17.7075C16.6146 17.8004 16.5043 17.8741 16.3829 17.9244C16.2615 17.9747 16.1314 18.0006 16 18.0006C15.8686 18.0006 15.7385 17.9747 15.6171 17.9244C15.4957 17.8741 15.3854 17.8004 15.2925 17.7075C15.1996 17.6146 15.1259 17.5043 15.0756 17.3829C15.0253 17.2615 14.9994 17.1314 14.9994 17C14.9994 16.8686 15.0253 16.7385 15.0756 16.6171C15.1259 16.4957 15.1996 16.3854 15.2925 16.2925L20.2925 11.2925C20.3854 11.1995 20.4957 11.1258 20.6171 11.0754C20.7385 11.0251 20.8686 10.9992 21 10.9992C21.1314 10.9992 21.2615 11.0251 21.3829 11.0754C21.5043 11.1258 21.6146 11.1995 21.7075 11.2925ZM12 2C12 1.73478 12.1054 1.48043 12.2929 1.29289C12.4804 1.10536 12.7348 1 13 1H19C19.2652 1 19.5196 1.10536 19.7071 1.29289C19.8946 1.48043 20 1.73478 20 2C20 2.26522 19.8946 2.51957 19.7071 2.70711C19.5196 2.89464 19.2652 3 19 3H13C12.7348 3 12.4804 2.89464 12.2929 2.70711C12.1054 2.51957 12 2.26522 12 2Z"
fill="green"
/>
</svg>
</div>
<div className="text-[12px]">Edit Time Slots</div>
</div>
{canEnable && (
<div
className="flex gap-[12px] items-center"

View File

@ -0,0 +1,191 @@
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import dayjs from 'dayjs';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { Select } from '@gitroom/react/form/select';
import { Button } from '@gitroom/react/form/button';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
// @ts-ignore
import useKeypress from 'react-use-keypress';
import { useModals } from '@mantine/modals';
import { sortBy } from 'lodash';
dayjs.extend(utc);
dayjs.extend(timezone);
const hours = [...Array(24).keys()].map((i, index) => ({
value: index,
}));
const minutes = [...Array(60).keys()].map((i, index) => ({
value: index,
}));
export const TimeTable: FC<{
integration: Integrations;
mutate: () => void;
}> = (props) => {
const {
integration: { time },
mutate,
} = props;
const [currentTimes, setCurrentTimes] = useState([...time]);
const [hour, setHour] = useState(0);
const [minute, setMinute] = useState(0);
const fetch = useFetch();
const modal = useModals();
const askClose = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to close the window?',
'Yes, close'
))
) {
return;
}
modal.closeAll();
}, []);
useKeypress('Escape', askClose);
const removeSlot = useCallback(
(index: number) => async () => {
if (!(await deleteDialog('Are you sure you want to delete this slot?'))) {
return;
}
setCurrentTimes((prev) => prev.filter((_, i) => i !== index));
},
[]
);
const addHour = useCallback(() => {
const calculateMinutes =
dayjs()
.utc()
.startOf('day')
.add(hour, 'hours')
.add(minute, 'minutes')
.diff(dayjs().utc().startOf('day'), 'minutes') - dayjs.tz().utcOffset();
setCurrentTimes((prev) => [...prev, { time: calculateMinutes }]);
}, [hour, minute]);
const times = useMemo(() => {
return sortBy(
currentTimes.map(({ time }) => ({
value: time,
formatted: dayjs
.utc()
.startOf('day')
.add(time, 'minutes')
.local()
.format('HH:mm'),
})),
(p) => p.value
);
}, [currentTimes]);
const save = useCallback(async () => {
await fetch(`/integrations/${props.integration.id}/time`, {
method: 'POST',
body: JSON.stringify({ time: currentTimes }),
});
mutate();
modal.closeAll();
}, [currentTimes]);
return (
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
<TopTitle title={`Time Table Slots`} />
<button
onClick={askClose}
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"
>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
<div>
<div className="text-[16px] font-bold mt-[16px]">Add Time Slot</div>
<div className="flex flex-col">
<div className="mt-[16px] flex justify-center gap-[16px]">
<div className="w-[100px]">
<Select
label="Hour"
name="hour"
disableForm={true}
className="w-[100px] mt-[8px]"
value={hour}
onChange={(e) => setHour(Number(e.target.value))}
>
{hours.map((hour) => (
<option key={hour.value} value={hour.value}>
{hour.value.toString().length === 1 ? '0' : ''}
{hour.value}
</option>
))}
</Select>
</div>
<div className="w-[100px]">
<Select
label="Minutes"
name="minutes"
disableForm={true}
className="w-[100px] mt-[8px]"
value={minute}
onChange={(e) => setMinute(Number(e.target.value))}
>
{minutes.map((minute) => (
<option key={minute.value} value={minute.value}>
{minute.value.toString().length === 1 ? '0' : ''}
{minute.value}
</option>
))}
</Select>
</div>
</div>
<div className="flex w-[215px] mx-auto justify-center mb-[50px]">
<Button type="button" className="w-full" onClick={addHour}>
Add Slot
</Button>
</div>
</div>
</div>
<div className="mt-[16px] grid grid-cols-2 place-items-center w-[100px] mx-auto">
{times.map((timeSlot, index) => (
<Fragment key={timeSlot.formatted}>
<div className="text-left w-full">{timeSlot.formatted}</div>
<div
className="cursor-pointer text-red-400 text-left w-full"
onClick={removeSlot(index)}
>
X
</div>
</Fragment>
))}
</div>
<div className="flex w-[215px] mx-auto justify-center mb-[50px]">
<Button type="button" className="w-full" onClick={save}>
Save
</Button>
</div>
</div>
);
};

View File

@ -1,11 +1,11 @@
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import dayjs from 'dayjs';
import * as console from 'node:console';
import { Integration } from '@prisma/client';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader';
import axios from 'axios';
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
@Injectable()
export class IntegrationRepository {
@ -14,10 +14,34 @@ export class IntegrationRepository {
private _posts: PrismaRepository<'post'>
) {}
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
return this._integration.model.integration.update({
select: {
id: true,
},
where: {
id,
organizationId: org,
},
data: {
postingTimes: JSON.stringify(times.time),
},
});
}
async updateIntegration(id: string, params: Partial<Integration>) {
if (params.picture && params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1) {
const picture = await axios.get(params.picture, { responseType: 'arraybuffer' });
params.picture = await simpleUpload(picture.data, `${makeId(10)}.png`, 'image/png');
if (
params.picture &&
params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1
) {
const picture = await axios.get(params.picture, {
responseType: 'arraybuffer',
});
params.picture = await simpleUpload(
picture.data,
`${makeId(10)}.png`,
'image/png'
);
}
return this._integration.model.integration.update({
@ -54,8 +78,18 @@ export class IntegrationRepository {
expiresIn = 999999999,
username?: string,
isBetweenSteps = false,
refresh?: string
refresh?: string,
timezone?: number
) {
const postTimes = timezone
? {
postingTimes: JSON.stringify([
{ time: 560 - timezone },
{ time: 850 - timezone },
{ time: 1140 - timezone },
]),
}
: {};
return this._integration.model.integration.upsert({
where: {
organizationId_internalId: {
@ -76,6 +110,7 @@ export class IntegrationRepository {
? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
: {}),
internalId,
...postTimes,
organizationId: org,
refreshNeeded: false,
},
@ -212,7 +247,7 @@ export class IntegrationRepository {
where: {
organizationId: org,
integrationId: id,
deletedAt: null
deletedAt: null,
},
});
}

View File

@ -16,11 +16,11 @@ import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/soc
import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader';
import axios from 'axios';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import dayjs from 'dayjs';
import { timer } from '@gitroom/helpers/utils/timer';
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
@Injectable()
export class IntegrationService {
@ -29,6 +29,11 @@ export class IntegrationService {
private _integrationManager: IntegrationManager,
private _notificationService: NotificationService
) {}
async setTimes(orgId: string, integrationId: string, times: IntegrationTimeDto) {
return this._integrationRepository.setTimes(orgId, integrationId, times);
}
async createOrUpdateIntegration(
org: string,
name: string,
@ -41,7 +46,8 @@ export class IntegrationService {
expiresIn?: number,
username?: string,
isBetweenSteps = false,
refresh?: string
refresh?: string,
timezone?: number
) {
const loadImage = await axios.get(picture, { responseType: 'arraybuffer' });
const uploadedPicture = await simpleUpload(
@ -62,7 +68,8 @@ export class IntegrationService {
expiresIn,
username,
isBetweenSteps,
refresh
refresh,
timezone
);
}

View File

@ -64,10 +64,31 @@ export class PostsRepository {
getPosts(orgId: string, query: GetPostsDto) {
const dateYear = dayjs().year(query.year);
const date = query.week ? dateYear.isoWeek(query.week) : dateYear.month(query.month-1);
const date =
query.display === 'day'
? dateYear.isoWeek(query.week).day(query.day)
: query.display === 'week'
? dateYear.isoWeek(query.week)
: dateYear.month(query.month - 1);
const startDate = (query.week ? date.startOf('isoWeek') : date.startOf('month')).subtract(2, 'days').toDate();
const endDate = (query.week ? date.endOf('isoWeek') : date.endOf('month')).add(2, 'days').toDate();
const startDate = (
query.display === 'day'
? date.startOf('day')
: query.display === 'week'
? date.startOf('isoWeek')
: date.startOf('month')
)
.subtract(2, 'hours')
.toDate();
const endDate = (
query.display === 'day'
? date.endOf('day')
: query.display === 'week'
? date.endOf('isoWeek')
: date.endOf('month')
)
.add(2, 'hours')
.toDate();
return this._post.model.post.findMany({
where: {

View File

@ -262,6 +262,7 @@ model Integration {
orderItems OrderItems[]
inBetweenSteps Boolean @default(false)
refreshNeeded Boolean @default(false)
postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
@@index([updatedAt])
@@index([deletedAt])

View File

@ -1,15 +1,19 @@
import { IsDefined, IsOptional, IsString } from 'class-validator';
export class ConnectIntegrationDto {
@IsString()
@IsDefined()
state: string;
@IsString()
@IsDefined()
state: string;
@IsString()
@IsDefined()
code: string;
@IsString()
@IsDefined()
code: string;
@IsString()
@IsOptional()
refresh?: string;
}
@IsString()
@IsDefined()
timezone: string;
@IsString()
@IsOptional()
refresh?: string;
}

View File

@ -0,0 +1,15 @@
import { IsArray, IsDefined, IsNumber, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
export class IntegrationValidateTimeDto {
@IsDefined()
@IsNumber()
time: number;
}
export class IntegrationTimeDto {
@Type(() => IntegrationValidateTimeDto)
@IsArray()
@IsDefined()
@ValidateNested({each: true})
time: IntegrationValidateTimeDto[];
}

View File

@ -1,16 +1,30 @@
import { Type } from 'class-transformer';
import { IsIn, IsNumber, IsString, Max, Min, ValidateIf } from 'class-validator';
import {
IsDefined,
IsIn,
IsNumber,
Max,
Min,
} from 'class-validator';
import dayjs from 'dayjs';
export class GetPostsDto {
@ValidateIf((o) => !o.month)
@Type(() => Number)
@IsNumber()
@Max(52)
@Min(1)
week: number;
@ValidateIf((o) => !o.week)
@Type(() => Number)
@IsNumber()
@Max(6)
@Min(0)
day: number;
@IsDefined()
@IsIn(['day', 'week', 'month'])
display: 'day' | 'week' | 'month';
@Type(() => Number)
@IsNumber()
@Max(52)