diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index 5e0ecc97..133758d0 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -14,7 +14,6 @@ import { SettingsController } from '@gitroom/backend/api/routes/settings.control import { PostsController } from '@gitroom/backend/api/routes/posts.controller'; import { MediaController } from '@gitroom/backend/api/routes/media.controller'; import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module'; -import { CommentsController } from '@gitroom/backend/api/routes/comments.controller'; import { BillingController } from '@gitroom/backend/api/routes/billing.controller'; import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller'; import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.controller'; @@ -35,7 +34,6 @@ const authenticatedController = [ SettingsController, PostsController, MediaController, - CommentsController, BillingController, NotificationsController, MarketplaceController, diff --git a/apps/backend/src/api/routes/comments.controller.ts b/apps/backend/src/api/routes/comments.controller.ts deleted file mode 100644 index 8669a688..00000000 --- a/apps/backend/src/api/routes/comments.controller.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common'; -import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service'; -import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; -import { Organization, User } from '@prisma/client'; -import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; -import { AddCommentDto } from '@gitroom/nestjs-libraries/dtos/comments/add.comment.dto'; -import {ApiTags} from "@nestjs/swagger"; - -@ApiTags('Comments') -@Controller('/comments') -export class CommentsController { - constructor(private _commentsService: CommentsService) {} - - @Post('/') - addComment( - @GetOrgFromRequest() org: Organization, - @GetUserFromRequest() user: User, - @Body() addCommentDto: AddCommentDto - ) { - return this._commentsService.addAComment( - org.id, - user.id, - addCommentDto.content, - addCommentDto.date - ); - } - - @Post('/:id') - addCommentTocComment( - @GetOrgFromRequest() org: Organization, - @GetUserFromRequest() user: User, - @Body() addCommentDto: AddCommentDto, - @Param('id') id: string - ) { - return this._commentsService.addACommentToComment( - org.id, - user.id, - id, - addCommentDto.content, - addCommentDto.date - ); - } - - @Put('/:id') - editComment( - @GetOrgFromRequest() org: Organization, - @GetUserFromRequest() user: User, - @Body() addCommentDto: AddCommentDto, - @Param('id') id: string - ) { - return this._commentsService.updateAComment( - org.id, - user.id, - id, - addCommentDto.content - ); - } - - @Delete('/:id') - deleteComment( - @GetOrgFromRequest() org: Organization, - @GetUserFromRequest() user: User, - @Param('id') id: string - ) { - return this._commentsService.deleteAComment( - org.id, - user.id, - id, - ); - } - - @Get('/:date') - loadAllCommentsAndSubCommentsForADate( - @GetOrgFromRequest() org: Organization, - @Param('date') date: string - ) { - return this._commentsService.loadAllCommentsAndSubCommentsForADate( - org.id, - date - ); - } -} diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index 1aea47c7..314ad5c6 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -26,6 +26,7 @@ import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto'; import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service'; import { Response } from 'express'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; @ApiTags('Posts') @Controller('/posts') @@ -45,10 +46,14 @@ export class PostsController { return this._messagesService.getMarketplaceAvailableOffers(org.id, id); } - @Post('/posts/generate-image') - @CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH]) - generateImage(@Body() body: { text: string; type: string }) { - + @Post('/:id/comments') + async createComment( + @GetOrgFromRequest() org: Organization, + @GetUserFromRequest() user: User, + @Param('id') id: string, + @Body() body: { comment: string } + ) { + return this._postsService.createComment(org.id, user.id, id, body.comment); } @Get('/') @@ -71,6 +76,13 @@ export class PostsController { }; } + @Get('/find-slot') + async findSlot( + @GetOrgFromRequest() org: Organization, + ) { + return {date: await this._postsService.findFreeDateTime(org.id)} + } + @Get('/predict-trending') predictTrending() { return this._starsService.predictTrending(); diff --git a/apps/backend/src/api/routes/public.controller.ts b/apps/backend/src/api/routes/public.controller.ts index 44ff199f..604178fb 100644 --- a/apps/backend/src/api/routes/public.controller.ts +++ b/apps/backend/src/api/routes/public.controller.ts @@ -1,16 +1,14 @@ import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service'; +import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; import { TrackService } from '@gitroom/nestjs-libraries/track/track.service'; import { RealIP } from 'nestjs-real-ip'; import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent'; import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum'; import { Request, Response } from 'express'; -import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; -import { User } from '@prisma/client'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; -import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service'; import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service'; @ApiTags('Public') @@ -19,7 +17,8 @@ export class PublicController { constructor( private _agenciesService: AgenciesService, private _trackService: TrackService, - private _agentGraphInsertService: AgentGraphInsertService + private _agentGraphInsertService: AgentGraphInsertService, + private _postsService: PostsService ) {} @Post('/agent') async createAgent(@Body() body: { text: string; apiKey: string }) { @@ -53,6 +52,31 @@ export class PublicController { return this._agenciesService.getCount(); } + @Get(`/posts/:id`) + async getPreview(@Param('id') id: string) { + return (await this._postsService.getPostsRecursively(id, true)).map( + ({ childrenPost, ...p }) => ({ + ...p, + ...(p.integration + ? { + integration: { + id: p.integration.id, + name: p.integration.name, + picture: p.integration.picture, + providerIdentifier: p.integration.providerIdentifier, + profile: p.integration.profile, + }, + } + : {}), + }) + ); + } + + @Get(`/posts/:id/comments`) + async getComments(@Param('id') postId: string) { + return { comments: await this._postsService.getComments(postId) }; + } + @Post('/t') async trackEvent( @Res() res: Response, diff --git a/apps/frontend/src/app/(preview)/p/[id]/layout.tsx b/apps/frontend/src/app/(preview)/p/[id]/layout.tsx new file mode 100644 index 00000000..6a60756a --- /dev/null +++ b/apps/frontend/src/app/(preview)/p/[id]/layout.tsx @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; +import { PreviewWrapper } from '@gitroom/frontend/components/preview/preview.wrapper'; + +export default async function AppLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/apps/frontend/src/app/(preview)/p/[id]/page.tsx b/apps/frontend/src/app/(preview)/p/[id]/page.tsx new file mode 100644 index 00000000..c31686cc --- /dev/null +++ b/apps/frontend/src/app/(preview)/p/[id]/page.tsx @@ -0,0 +1,170 @@ +import { internalFetch } from '@gitroom/helpers/utils/internal.fetch'; + +export const dynamic = 'force-dynamic'; + +import { Metadata } from 'next'; +import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side'; +import Image from 'next/image'; +import Link from 'next/link'; +import { CommentsComponents } from '@gitroom/frontend/components/preview/comments.components'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { VideoOrImage } from '@gitroom/react/helpers/video.or.image'; +import { CopyClient } from '@gitroom/frontend/components/preview/copy.client'; + +dayjs.extend(utc); +export const metadata: Metadata = { + title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Preview`, + description: '', +}; + +export default async function Auth({ + params: { id }, + searchParams, +}: { + params: { id: string }; + searchParams?: { share?: string }; +}) { + const post = await (await internalFetch(`/public/posts/${id}`)).json(); + + if (!post.length) { + return ( +
+ Post not found +
+ ); + } + + return ( +
+
+
+
+
+
+ +
+ Logo +
+
+ + + + + + +
+ +
+
+
+
+ {!!searchParams?.share && ( +
+ +
+ )} +
+ Publication Date:{' '} + {dayjs + .utc(post[0].createdAt) + .local() + .format('MMMM D, YYYY h:mm A')} +
+
+
+
+ +
+
+
+ {post.map((p: any, index: number) => ( +
+
+
+
+
+ {post[0].integration.name} +
+
+ {post[0].integration.providerIdentifier} +
+
+
+
+
+

+ {post[0].integration.name} +

+ + @{post[0].integration.profile} + +
+
+
+
+ {JSON.parse(p?.image || '[]').map((p: any) => ( +
+ +
+ ))} +
+
+
+
+
+ ))} +
+
+
+
+ +
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index f7e58044..2cdbcb30 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -38,8 +38,25 @@ export const AddProviderButton: FC<{ update?: () => void }> = (props) => { const { update } = props; const add = useAddProvider(update); return ( - ); }; diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index e067b269..5cbbdf7f 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -311,12 +311,15 @@ export const CalendarColumn: FC<{ return getDate.startOf('hour').isBefore(dayjs().startOf('hour')); }, [getDate, num]); - const { start, stop } = useInterval(useCallback(() => { - if (isBeforeNow) { - return ; - } - setNum(num + 1); - }, [isBeforeNow]), random(120000, 150000)); + const { start, stop } = useInterval( + useCallback(() => { + if (isBeforeNow) { + return; + } + setNum(num + 1); + }, [isBeforeNow]), + random(120000, 150000) + ); useEffect(() => { start(); @@ -401,12 +404,15 @@ export const CalendarColumn: FC<{ ); const editPost = useCallback( - (post: Post & { integration: Integration }) => async () => { + (post: Post & { integration: Integration }, isDuplicate?: boolean) => async () => { if (user?.orgId === post.submittedForOrganizationId) { return previewPublication(post); } const data = await (await fetch(`/posts/${post.id}`)).json(); - const publishDate = dayjs.utc(data.posts[0].publishDate).local(); + const date = !isDuplicate ? null : (await (await fetch('/posts/find-slot')).json()).date; + const publishDate = dayjs.utc(date || data.posts[0].publishDate).local(); + + const ExistingData = !isDuplicate ? ExistingDataContextProvider : Fragment; modal.openModal({ closeOnClickOutside: false, @@ -416,18 +422,19 @@ export const CalendarColumn: FC<{ modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor', }, children: ( - + ({ ...p }))} reopenModal={editPost(post)} mutate={reloadCalendarView} - integrations={integrations + integrations={isDuplicate ? integrations : integrations .slice(0) .filter((f) => f.id === data.integration) .map((p) => ({ ...p, picture: data.integrationPicture }))} date={publishDate} /> - + ), size: '80%', title: ``, @@ -484,7 +491,7 @@ export const CalendarColumn: FC<{ } : {})} className={clsx( - 'flex-col text-[12px] pointer w-full overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary', + 'flex-col text-[12px] pointer w-full flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary', isBeforeNow ? 'bg-customColor23 flex-1' : 'cursor-pointer', isBeforeNow && postList.length === 0 && 'col-calendar', canBeTrending && 'bg-customColor24' @@ -497,13 +504,14 @@ export const CalendarColumn: FC<{ 'text-textColor p-[2.5px] relative flex flex-col justify-center items-center' )} > -
+
@@ -588,12 +596,18 @@ const CalendarItem: FC<{ date: dayjs.Dayjs; isBeforeNow: boolean; editPost: () => void; + duplicatePost: () => void; integrations: Integrations[]; state: State; display: 'day' | 'week' | 'month'; post: Post & { integration: Integration }; }> = (props) => { - const { editPost, post, date, isBeforeNow, state, display } = props; + const { editPost, duplicatePost, post, date, isBeforeNow, state, display } = props; + + const preview = useCallback(() => { + window.open(`/p/` + post.id + '?share=true', '_blank'); + }, [post]); + const [{ opacity }, dragRef] = useDrag( () => ({ type: 'post', @@ -608,33 +622,41 @@ const CalendarItem: FC<{
+
+
Duplicate
+
Preview
+
- - -
-
-
{state === 'DRAFT' ? 'Draft: ' : ''}
-
- {removeMd(post.content).replace(/\n/g, ' ')} +
+ + +
+
+
{state === 'DRAFT' ? 'Draft: ' : ''}
+
+ {removeMd(post.content).replace(/\n/g, ' ')} +
diff --git a/apps/frontend/src/components/launches/generator/generator.tsx b/apps/frontend/src/components/launches/generator/generator.tsx index 5237f0c3..91f17d5a 100644 --- a/apps/frontend/src/components/launches/generator/generator.tsx +++ b/apps/frontend/src/components/launches/generator/generator.tsx @@ -320,7 +320,7 @@ export const GeneratorComponent = () => { fill="currentColor" /> - Generate Posts +
Generate Posts
); }; diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index ce6433a6..129e76bf 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -22,6 +22,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider'; import { GeneratorComponent } from './generator/generator'; import { useVariables } from '@gitroom/react/helpers/variable.context'; +import { NewPost } from '@gitroom/frontend/components/launches/new.post'; interface MenuComponentInterface { refreshChannel: ( @@ -120,14 +121,22 @@ export const MenuGroupComponent: FC<
)} {!!group.name && ( -
+
{group.name}
)} -
+
{group.values.map((integration) => ( { /> ))}
- update(true)} /> - {sortedIntegrations?.length > 0 && - user?.tier?.ai && - billingEnabled && } +
+ update(true)} /> + {sortedIntegrations?.length > 0 && } + {sortedIntegrations?.length > 0 && + user?.tier?.ai && + billingEnabled && } +
diff --git a/apps/frontend/src/components/launches/new.post.tsx b/apps/frontend/src/components/launches/new.post.tsx new file mode 100644 index 00000000..51d5a456 --- /dev/null +++ b/apps/frontend/src/components/launches/new.post.tsx @@ -0,0 +1,56 @@ +import React, { useCallback } from 'react'; +import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model'; +import { useModals } from '@mantine/modals'; +import dayjs from 'dayjs'; +import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; + +export const NewPost = () => { + const fetch = useFetch(); + const modal = useModals(); + const { integrations, reloadCalendarView } = useCalendar(); + + const createAPost = useCallback(async () => { + const date = (await (await fetch('/posts/find-slot')).json()).date; + modal.openModal({ + closeOnClickOutside: false, + closeOnEscape: false, + withCloseButton: false, + classNames: { + modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor', + }, + children: ( + ({ ...p }))} + reopenModal={createAPost} + mutate={reloadCalendarView} + integrations={integrations} + date={dayjs.utc(date).local()} + /> + ), + size: '80%', + title: ``, + }); + }, []); + + return ( + + ); +}; diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx index 145edb36..1608c002 100644 --- a/apps/frontend/src/components/layout/layout.context.tsx +++ b/apps/frontend/src/components/layout/layout.context.tsx @@ -20,6 +20,10 @@ function LayoutContextInner(params: { children: ReactNode }) { const afterRequest = useCallback( async (url: string, options: RequestInit, response: Response) => { + if (typeof window !== 'undefined' && window.location.href.includes('/p/')) { + return true; + } + const reloadOrOnboarding = response?.headers?.get('reload') || response?.headers?.get('onboarding'); diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 0bc19cb7..b476e479 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -70,167 +70,162 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { credentials="include" runtimeUrl={backendUrl + '/copilot/chat'} > - - - {user.tier === 'FREE' && searchParams.get('check') && ( - - )} - - - - - - - {user.tier !== 'FREE' && } - - -
- {user?.admin && } -