diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index a633bb4d..a9a92b2d 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -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 ); } diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index dccdd28c..1590411a 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -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 ) {} diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx new file mode 100644 index 00000000..bead77f8 --- /dev/null +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx @@ -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}`); +} diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx index bead77f8..f8f17873 100644 --- a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx @@ -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 ; } diff --git a/apps/frontend/src/app/(site)/integrations/social/layout.tsx b/apps/frontend/src/app/(site)/integrations/social/layout.tsx new file mode 100644 index 00000000..39b81264 --- /dev/null +++ b/apps/frontend/src/app/(site)/integrations/social/layout.tsx @@ -0,0 +1,13 @@ +import { ReactNode } from 'react'; + +export default async function IntegrationLayout({ + children, +}: { + children: ReactNode; +}) { + return ( +
+ Adding channel, Redirecting You{children} +
+ ); +} diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index e4996b45..966a2db8 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -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" /> )} -
+
- {!existingData.integration && ( + {!existingData.integration && integrations.length > 1 ? ( !f.disabled)} selectedIntegrations={[]} @@ -410,6 +416,35 @@ export const AddEditModal: FC<{ onChange={setSelectedIntegrations} isMain={true} /> + ) : ( +
+ {selectedIntegrations?.[0]?.identifier} + {selectedIntegrations?.[0]?.identifier === 'youtube' ? ( + + ) : ( + {selectedIntegrations?.[0]?.identifier} + )} +
)}
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 diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index 724dd29a..901b02f5 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -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, - 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([]); 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] ); diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index c06fa655..93ea124b 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -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 ( +
+ {options.map((option) => ( + +
+ {dayjs() + .utc() + .startOf('day') + .add(option[0].time, 'minute') + .local() + .format('HH:mm')} +
+
+ p.integration), + }} + > + + +
+
+ ))} +
+ ); +}; + export const WeekView = () => { const { currentYear, currentWeek } = useCalendar(); @@ -155,7 +231,13 @@ export const Calendar = () => { return ( - {display === 'week' ? : } + {display === 'day' ? ( + + ) : display === 'week' ? ( + + ) : ( + + )} ); }; @@ -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<{ >
))}
- {!isBeforeNow && ( + {(display === 'day' + ? !isBeforeNow && postList.length === 0 + : !isBeforeNow) && (
-
+ {display !== 'day' && ( +
+ )} + {display === 'day' && ( +
+ {integrations.map((selectedIntegrations) => ( +
+
+ {selectedIntegrations.identifier} + {selectedIntegrations.identifier === 'youtube' ? ( + + ) : ( + {selectedIntegrations.identifier} + )} +
+
+ ))} +
+ )}
)} @@ -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 }} > -
+
diff --git a/apps/frontend/src/components/launches/filters.tsx b/apps/frontend/src/components/launches/filters.tsx index a789fea8..f399ecbd 100644 --- a/apps/frontend/src/components/launches/filters.tsx +++ b/apps/frontend/src/components/launches/filters.tsx @@ -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 (
@@ -118,7 +161,13 @@ export const Filters = () => {
- {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')}`}
@@ -137,6 +186,15 @@ export const Filters = () => {
{betweenDates}
+
+ Day +
{ + 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; +}; diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index a9edc5d2..d0a5ef52 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -196,6 +196,7 @@ export const LaunchesComponent = () => { {integration.name}
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(() => { 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: ( + + ), + }); + setShow(false); + }, [integrations]); + return (
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`} > +
+
+ + + +
+
Edit Time Slots
+
{canEnable && (
({ + 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 ( +
+ + + +
+
Add Time Slot
+
+
+
+ +
+
+ +
+
+
+ +
+
+
+
+ {times.map((timeSlot, index) => ( + +
{timeSlot.formatted}
+
+ X +
+
+ ))} +
+
+ +
+
+ ); +}; diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index b986538c..7778c05a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -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) { - 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, }, }); } diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 5b7fc3ce..c67d7c9e 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -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 ); } diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index b4704b29..d213a6f4 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -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: { diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 079e0e74..3944ec4d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -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]) diff --git a/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts b/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts index 28bf4cb9..a74bfb5e 100644 --- a/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/integrations/connect.integration.dto.ts @@ -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; -} \ No newline at end of file + @IsString() + @IsDefined() + timezone: string; + + @IsString() + @IsOptional() + refresh?: string; +} diff --git a/libraries/nestjs-libraries/src/dtos/integrations/integration.time.dto.ts b/libraries/nestjs-libraries/src/dtos/integrations/integration.time.dto.ts new file mode 100644 index 00000000..cde4d0b6 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/integrations/integration.time.dto.ts @@ -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[]; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts index 1a89560f..258fa694 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts @@ -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)