diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index e01b7e05..e7e8d9a2 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -16,7 +16,8 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; 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 {ServeStaticModule} from "@nestjs/serve-static"; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { CommentsController } from '@gitroom/backend/api/routes/comments.controller'; const authenticatedController = [ UsersController, @@ -25,6 +26,7 @@ const authenticatedController = [ SettingsController, PostsController, MediaController, + CommentsController, ]; @Module({ imports: [ @@ -37,7 +39,7 @@ const authenticatedController = [ serveRoot: '/' + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY, serveStaticOptions: { index: false, - } + }, }), ], controllers: [StripeController, AuthController, ...authenticatedController], diff --git a/apps/backend/src/api/routes/comments.controller.ts b/apps/backend/src/api/routes/comments.controller.ts new file mode 100644 index 00000000..0a0f9fcb --- /dev/null +++ b/apps/backend/src/api/routes/comments.controller.ts @@ -0,0 +1,80 @@ +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'; + +@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 aa386946..f79212fb 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -1,47 +1,74 @@ -import {Body, Controller, Get, Param, Post, Put, Query} from '@nestjs/common'; -import {PostsService} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.service"; -import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request"; -import {Organization} 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 { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, +} from '@nestjs/common'; +import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; +import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { Organization } 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'; @Controller('/posts') export class PostsController { - constructor( - private _postsService: PostsService - ) { - } + constructor( + private _postsService: PostsService, + private _commentsService: CommentsService + ) {} - @Get('/') - getPosts( - @GetOrgFromRequest() org: Organization, - @Query() query: GetPostsDto - ) { - return this._postsService.getPosts(org.id, query); - } + @Get('/') + async getPosts( + @GetOrgFromRequest() org: Organization, + @Query() query: GetPostsDto + ) { + const [posts, comments] = await Promise.all([ + this._postsService.getPosts(org.id, query), + this._commentsService.getAllCommentsByWeekYear( + org.id, + query.year, + query.week + ), + ]); - @Get('/:id') - getPost( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string, - ) { - return this._postsService.getPost(org.id, id); - } + return { + posts, + comments, + }; + } - @Post('/') - createPost( - @GetOrgFromRequest() org: Organization, - @Body() body: CreatePostDto - ) { - return this._postsService.createPost(org.id, body); - } + @Get('/:id') + getPost(@GetOrgFromRequest() org: Organization, @Param('id') id: string) { + return this._postsService.getPost(org.id, id); + } - @Put('/:id/date') - changeDate( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string, - @Body('date') date: string - ) { - return this._postsService.changeDate(org.id, id, date); - } + @Post('/') + createPost( + @GetOrgFromRequest() org: Organization, + @Body() body: CreatePostDto + ) { + return this._postsService.createPost(org.id, body); + } + + @Delete('/:group') + deletePost( + @GetOrgFromRequest() org: Organization, + @Param('group') group: string + ) { + return this._postsService.deletePost(org.id, group); + } + + @Put('/:id/date') + changeDate( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Body('date') date: string + ) { + return this._postsService.changeDate(org.id, id, date); + } } diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index 413502ba..42a09318 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -23,6 +23,7 @@ export class AuthMiddleware implements NestMiddleware { delete user.password; const organization = await this._organizationService.getFirstOrgByUserId(user.id); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error req.user = user; diff --git a/apps/frontend/public/auth/bg-login.png b/apps/frontend/public/auth/bg-login.png new file mode 100644 index 00000000..d79ca79b Binary files /dev/null and b/apps/frontend/public/auth/bg-login.png differ diff --git a/apps/frontend/public/auth/login-box.png b/apps/frontend/public/auth/login-box.png new file mode 100644 index 00000000..8bb77897 Binary files /dev/null and b/apps/frontend/public/auth/login-box.png differ diff --git a/apps/frontend/public/favicon.png b/apps/frontend/public/favicon.png new file mode 100755 index 00000000..57d731fd Binary files /dev/null and b/apps/frontend/public/favicon.png differ diff --git a/apps/frontend/public/logo.svg b/apps/frontend/public/logo.svg new file mode 100644 index 00000000..b570b398 --- /dev/null +++ b/apps/frontend/public/logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/frontend/src/app/(site)/analytics/page.tsx b/apps/frontend/src/app/(site)/analytics/page.tsx index 5fb26a1b..f641396d 100644 --- a/apps/frontend/src/app/(site)/analytics/page.tsx +++ b/apps/frontend/src/app/(site)/analytics/page.tsx @@ -1,5 +1,11 @@ import {AnalyticsComponent} from "@gitroom/frontend/components/analytics/analytics.component"; import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; +import {Metadata} from "next"; + +export const metadata: Metadata = { + title: 'Gitroom Analytics', + description: '', +} export default async function Index() { const analytics = await (await internalFetch('/analytics')).json(); diff --git a/apps/frontend/src/app/(site)/err/page.tsx b/apps/frontend/src/app/(site)/err/page.tsx index c89fe5bb..d6a16274 100644 --- a/apps/frontend/src/app/(site)/err/page.tsx +++ b/apps/frontend/src/app/(site)/err/page.tsx @@ -1,3 +1,10 @@ +import {Metadata} from "next"; + +export const metadata: Metadata = { + title: 'Error', + description: '', +} + export default async function Page() { return (
We are experiencing some difficulty, try to refresh the page
diff --git a/apps/frontend/src/app/(site)/launches/page.tsx b/apps/frontend/src/app/(site)/launches/page.tsx index 50acc836..cb351c5f 100644 --- a/apps/frontend/src/app/(site)/launches/page.tsx +++ b/apps/frontend/src/app/(site)/launches/page.tsx @@ -1,5 +1,11 @@ import {LaunchesComponent} from "@gitroom/frontend/components/launches/launches.component"; import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; +import {Metadata} from "next"; + +export const metadata: Metadata = { + title: 'Gitroom Launches', + description: '', +} export default async function Index() { const {integrations} = await (await internalFetch('/integrations/list')).json(); diff --git a/apps/frontend/src/app/(site)/settings/page.tsx b/apps/frontend/src/app/(site)/settings/page.tsx index 16f1fb8a..3d435470 100644 --- a/apps/frontend/src/app/(site)/settings/page.tsx +++ b/apps/frontend/src/app/(site)/settings/page.tsx @@ -2,7 +2,12 @@ import {SettingsComponent} from "@gitroom/frontend/components/settings/settings. import {internalFetch} from "@gitroom/helpers/utils/internal.fetch"; import {redirect} from "next/navigation"; import {RedirectType} from "next/dist/client/components/redirect"; +import {Metadata} from "next"; +export const metadata: Metadata = { + title: 'Gitroom Settings', + description: '', +} export default async function Index({searchParams}: {searchParams: {code: string}}) { if (searchParams.code) { await internalFetch('/settings/github', { diff --git a/apps/frontend/src/app/auth/layout.tsx b/apps/frontend/src/app/auth/layout.tsx index 427a1e10..82cc401e 100644 --- a/apps/frontend/src/app/auth/layout.tsx +++ b/apps/frontend/src/app/auth/layout.tsx @@ -1,16 +1,30 @@ -import '../global.css'; -import {ReactNode} from "react"; +import { ReactNode } from 'react'; -export default async function AuthLayout({children}: {children: ReactNode}) { - return ( -
-
-
-
- {children} +export default async function AuthLayout({ + children, +}: { + children: ReactNode; +}) { + return ( + <> +
+
+
+
+ {children} +
+
+
+
-
-
+
+
+
+
+
+
- ); -} \ No newline at end of file +
+ + ); +} diff --git a/apps/frontend/src/app/global.css b/apps/frontend/src/app/global.css index 4c853287..fb88051d 100644 --- a/apps/frontend/src/app/global.css +++ b/apps/frontend/src/app/global.css @@ -252,3 +252,29 @@ html { .react-tags__listbox-option-highlight { background-color: #ffdd00; } + +#renderEditor:not(:has(:first-child)) { + display: none; +} +.w-md-editor { + background-color: #131B2C !important; + border: 0 !important; + box-shadow: none !important; + border-radius: 8px !important; + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.w-md-editor-toolbar { + height: 40px !important; + min-height: 40px !important; + background-color: #131B2C !important; + padding: 0 8px !important; + border-color: #28344F !important; +} + +.wmde-markdown { + background: transparent !important; + font-size: 20px !important; + font-weight: 400 !important; +} diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index fe90cca4..029991a7 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -1,18 +1,19 @@ import './global.css'; -import 'react-tooltip/dist/react-tooltip.css' +import 'react-tooltip/dist/react-tooltip.css'; -import LayoutContext from "@gitroom/frontend/components/layout/layout.context"; -import {ReactNode} from "react"; -import {Chakra_Petch} from "next/font/google"; -const chakra = Chakra_Petch({weight: '400', subsets: ['latin']}) -export default async function AppLayout({children}: {children: ReactNode}) { - return ( - - - - {children} - - - - ) -} \ No newline at end of file +import LayoutContext from '@gitroom/frontend/components/layout/layout.context'; +import { ReactNode } from 'react'; +import { Chakra_Petch } from 'next/font/google'; +const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] }); +export default async function AppLayout({ children }: { children: ReactNode }) { + return ( + + + + + + {children} + + + ); +} diff --git a/apps/frontend/src/components/auth/login.tsx b/apps/frontend/src/components/auth/login.tsx index afe9335e..1dba973e 100644 --- a/apps/frontend/src/components/auth/login.tsx +++ b/apps/frontend/src/components/auth/login.tsx @@ -27,9 +27,9 @@ export function Login() { return (
-

Create An Account

+

Create An Account

-
+
diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx index 0d969b51..d8ddfc51 100644 --- a/apps/frontend/src/components/auth/register.tsx +++ b/apps/frontend/src/components/auth/register.tsx @@ -28,9 +28,9 @@ export function Register() { return (
-

Create An Account

+

Create An Account

-
+
diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 20253626..18e3a06b 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -3,198 +3,37 @@ import { FC, useCallback, useEffect, useState } from 'react'; import dayjs from 'dayjs'; import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; -import Image from 'next/image'; import clsx from 'clsx'; -import MDEditor from '@uiw/react-md-editor'; +import MDEditor, { commands } from '@uiw/react-md-editor'; import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload'; import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; import { useModals } from '@mantine/modals'; -import { ShowAllProviders } from '@gitroom/frontend/components/launches/providers/show.all.providers'; import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor'; import { Button } from '@gitroom/react/form/button'; -import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration'; +// @ts-ignore +import useKeypress from 'react-use-keypress'; import { getValues, resetValues, } from '@gitroom/frontend/components/launches/helpers/use.values'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; -import { - useMoveToIntegration, - useMoveToIntegrationListener, -} from '@gitroom/frontend/components/launches/helpers/use.move.to.integration'; +import { useMoveToIntegration } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration'; import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data'; +import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component'; +import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; +import { useExpend } from '@gitroom/frontend/components/launches/helpers/use.expend'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component'; +import { ProvidersOptions } from '@gitroom/frontend/components/launches/providers.options'; +import { v4 as uuidv4 } from 'uuid'; +import { useSWRConfig } from 'swr'; -export const PickPlatforms: FC<{ - integrations: Integrations[]; - selectedIntegrations: Integrations[]; - onChange: (integrations: Integrations[]) => void; - singleSelect: boolean; -}> = (props) => { - const { integrations, selectedIntegrations, onChange } = props; - const [selectedAccounts, setSelectedAccounts] = - useState(selectedIntegrations); - - useEffect(() => { - if ( - props.singleSelect && - selectedAccounts.length && - integrations.indexOf(selectedAccounts?.[0]) === -1 - ) { - addPlatform(integrations[0])(); - } - }, [integrations, selectedAccounts]); - - useMoveToIntegrationListener(props.singleSelect, (identifier) => { - const findIntegration = integrations.find( - (p) => p.identifier === identifier - ); - if (findIntegration) { - addPlatform(findIntegration)(); - } - }); - - const addPlatform = useCallback( - (integration: Integrations) => async () => { - if (props.singleSelect) { - onChange([integration]); - setSelectedAccounts([integration]); - return; - } - if (selectedAccounts.includes(integration)) { - const changedIntegrations = selectedAccounts.filter( - ({ id }) => id !== integration.id - ); - - if ( - !props.singleSelect && - !(await deleteDialog( - 'Are you sure you want to remove this platform?' - )) - ) { - return; - } - onChange(changedIntegrations); - setSelectedAccounts(changedIntegrations); - } else { - const changedIntegrations = [...selectedAccounts, integration]; - onChange(changedIntegrations); - setSelectedAccounts(changedIntegrations); - } - }, - [selectedAccounts] - ); - return ( -
- {integrations.map((integration) => - !props.singleSelect ? ( -
-
p.id === integration.id) === - -1 - ? 'grayscale opacity-65' - : 'grayscale-0' - )} - > - {integration.identifier} - {integration.identifier} -
-
- ) : ( -
-
p.id === integration.id) === - -1 - ? 'bg-sixth' - : 'bg-forth' - )} - > -
-
- {integration.identifier} - {integration.identifier} -
-
{integration.name}
-
-
-
- ) - )} -
- ); -}; - -export const PreviewComponent: FC<{ - integrations: Integrations[]; - editorValue: Array<{ id?: string; content: string }>; -}> = (props) => { - const { integrations, editorValue } = props; - const [selectedIntegrations, setSelectedIntegrations] = useState([ - integrations[0], - ]); - - useEffect(() => { - if (integrations.indexOf(selectedIntegrations[0]) === -1) { - setSelectedIntegrations([integrations[0]]); - } - }, [integrations, selectedIntegrations]); - return ( -
- - - - -
- ); -}; export const AddEditModal: FC<{ date: dayjs.Dayjs; integrations: Integrations[]; }> = (props) => { const { date, integrations } = props; + const { mutate } = useSWRConfig(); // selected integrations to allow edit const [selectedIntegrations, setSelectedIntegrations] = useState< @@ -202,9 +41,13 @@ export const AddEditModal: FC<{ >([]); // value of each editor - const [value, setValue] = useState>([ - { content: '' }, - ]); + const [value, setValue] = useState< + Array<{ + content: string; + id?: string; + image?: Array<{ id: string; path: string }>; + }> + >([{ content: '' }]); const fetch = useFetch(); @@ -217,12 +60,16 @@ export const AddEditModal: FC<{ // hook to test if the top editor should be hidden const showHide = useHideTopEditor(); + const [showError, setShowError] = useState(false); + // hook to open a new modal const modal = useModals(); // are we in edit mode? const existingData = useExistingData(); + const expend = useExpend(); + // if it's edit just set the current integration useEffect(() => { if (existingData.integration) { @@ -250,6 +97,19 @@ export const AddEditModal: FC<{ [value] ); + const changeImage = useCallback( + (index: number) => + (newValue: { + target: { name: string; value?: Array<{ id: string; path: string }> }; + }) => { + return setValue((prev) => { + prev[index].image = newValue.target.value; + return [...prev]; + }); + }, + [value] + ); + // Add another editor const addValue = useCallback( (index: number) => () => { @@ -261,6 +121,25 @@ export const AddEditModal: FC<{ [value] ); + // Delete post + const deletePost = useCallback( + (index: number) => async () => { + if ( + !(await deleteDialog( + 'Are you sure you want to delete this post?', + 'Yes, delete it!' + )) + ) { + return; + } + setValue((prev) => { + prev.splice(index, 1); + return [...prev]; + }); + }, + [value] + ); + // override the close modal to ask the user if he is sure to close const askClose = useCallback(async () => { if ( @@ -273,95 +152,257 @@ export const AddEditModal: FC<{ } }, []); - // function to send to the server and save - const schedule = useCallback(async () => { - const values = getValues(); - const allKeys = Object.keys(values).map((v) => ({ - integration: integrations.find((p) => p.id === v), - value: values[v].posts, - valid: values[v].isValid, - settings: values[v].settings(), - })); + // sometimes it's easier to click escape to close + useKeypress('Escape', askClose); - for (const key of allKeys) { - if (!key.valid) { - moveToIntegration(key?.integration?.identifier!); + // function to send to the server and save + const schedule = useCallback( + (type: 'draft' | 'now' | 'schedule' | 'delete') => async () => { + if (type === 'delete') { + if (!await deleteDialog('Are you sure you want to delete this post?', 'Yes, delete it!')) { + return ; + } + await fetch(`/posts/${existingData.group}`, { + method: 'DELETE', + }); + mutate('/posts'); + modal.closeAll(); return; } - } - await fetch('/posts', { - method: 'POST', - body: JSON.stringify({ - date: date.utc().format('YYYY-MM-DDTHH:mm:ss'), - posts: allKeys, - }), - }); - }, []); + const values = getValues(); + const allKeys = Object.keys(values).map((v) => ({ + integration: integrations.find((p) => p.id === v), + value: values[v].posts, + valid: values[v].isValid, + group: existingData?.group, + trigger: values[v].trigger, + settings: values[v].settings(), + })); + + for (const key of allKeys) { + if (key.value.some((p) => !p.content || p.content.length < 6)) { + setShowError(true); + return ; + } + + if (!key.valid) { + await key.trigger(); + moveToIntegration(key?.integration?.id!); + return; + } + } + + await fetch('/posts', { + method: 'POST', + body: JSON.stringify({ + type, + date: date.utc().format('YYYY-MM-DDTHH:mm:ss'), + posts: allKeys, + }), + }); + + existingData.group = uuidv4(); + + mutate('/posts'); + modal.closeAll(); + }, + [] + ); return ( <> - -
- {!existingData.integration && ( - - )} - {!existingData.integration && !showHide.hideTopEditor ? ( - <> - {value.map((p, index) => ( +
+ + + + + {!existingData.integration && ( + + )} +
+ {!existingData.integration && !showHide.hideTopEditor ? ( <> - 1 ? 150 : 500} - value={p.content} - preview="edit" - // @ts-ignore - onChange={changeValue(index)} - /> -
- -
+
You are in global editing mode
+ {value.map((p, index) => ( + <> +
+ 1 ? 150 : 250} + commands={[ + ...commands + .getCommands() + .filter((f) => f.name !== 'image'), + newImage, + ]} + value={p.content} + preview="edit" + // @ts-ignore + onChange={changeValue(index)} + /> + {showError && (!p.content || p.content.length < 6) && ( +
+ The post should be at least 6 characters long +
+ )} +
+
+ +
+
+ {value.length > 1 && ( +
+
+ + + +
+
+ Delete Post +
+
+ )} +
+
+
+
+ +
+ + ))} - ))} - - ) : ( - !existingData.integration && ( -
- Global Editor Hidden + ) : null} +
+
+
+
+ {!!existingData.integration && ( + + )} + + + +
- ) - )} - {!!selectedIntegrations.length && ( - - )} - +
+
+
+
+ +
+ {!!selectedIntegrations.length && ( +
+ +
+ )} +
); diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index b28edd9f..7761209f 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -1,109 +1,142 @@ -"use client"; +'use client'; -import {useModals} from "@mantine/modals"; -import {FC, useCallback} from "react"; -import {useFetch} from "@gitroom/helpers/utils/custom.fetch"; -import {Input} from "@gitroom/react/form/input"; -import {FieldValues, FormProvider, useForm} from "react-hook-form"; -import {Button} from "@gitroom/react/form/button"; +import { useModals } from '@mantine/modals'; +import { FC, useCallback } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Input } from '@gitroom/react/form/input'; +import { FieldValues, FormProvider, useForm } from 'react-hook-form'; +import { Button } from '@gitroom/react/form/button'; import { classValidatorResolver } from '@hookform/resolvers/class-validator'; -import {ApiKeyDto} from "@gitroom/nestjs-libraries/dtos/integrations/api.key.dto"; -import {useRouter} from "next/navigation"; +import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto'; +import { useRouter } from 'next/navigation'; const resolver = classValidatorResolver(ApiKeyDto); -export const AddProviderButton = () => { - const modal = useModals(); - const fetch = useFetch(); - const openModal = useCallback(async () => { - const data = await (await fetch('/integrations')).json(); - modal.openModal({ - title: 'Add Channel', - children: - }) - }, []); - return ( - - ); -} - -export const ApiModal: FC<{identifier: string, name: string}> = (props) => { - const fetch = useFetch(); - const router = useRouter(); - const modal = useModals(); - const methods = useForm({ - mode: 'onChange', - resolver +export const useAddProvider = () => { + const modal = useModals(); + const fetch = useFetch(); + return useCallback(async () => { + const data = await (await fetch('/integrations')).json(); + modal.openModal({ + title: 'Add Channel', + children: , }); + }, []); +}; - const submit = useCallback(async (data: FieldValues) => { - const add = await fetch(`/integrations/article/${props.identifier}/connect`, { - method: 'POST', - body: JSON.stringify({api: data.api}) - }); +export const AddProviderButton = () => { + const add = useAddProvider(); + return ( + + ); +}; - if (add.ok) { - modal.closeAll(); - router.refresh(); - return ; - } +export const ApiModal: FC<{ identifier: string; name: string }> = (props) => { + const fetch = useFetch(); + const router = useRouter(); + const modal = useModals(); + const methods = useForm({ + mode: 'onChange', + resolver, + }); - methods.setError('api', { - message: 'Invalid API key' - }); - }, []); + const submit = useCallback(async (data: FieldValues) => { + const add = await fetch( + `/integrations/article/${props.identifier}/connect`, + { + method: 'POST', + body: JSON.stringify({ api: data.api }), + } + ); - return ( - - -
-
- -
- ) -} -export const AddProviderComponent: FC<{social: Array<{identifier: string, name: string}>, article: Array<{identifier: string, name: string}>}> = (props) => { - const fetch = useFetch(); - const modal = useModals(); - const {social, article} = props; - const getSocialLink = useCallback((identifier: string) => async () => { - const {url} = await (await fetch('/integrations/social/' + identifier)).json(); - window.location.href = url; - }, []); + if (add.ok) { + modal.closeAll(); + router.refresh(); + return; + } - const showApiButton = useCallback((identifier: string, name: string) => async () => { - modal.openModal({ - title: `Add ${name}`, - children: - }) - }, []); - return ( -
-
-

Social

-
- {social.map((item) => ( -
- {item.name} -
- ))} -
-
-
-

Articles

-
- {article.map((item) => ( -
- {item.name} -
- ))} -
-
+ methods.setError('api', { + message: 'Invalid API key', + }); + }, []); + + return ( + +
+
+
- ) -} \ No newline at end of file +
+ +
+
+
+ ); +}; +export const AddProviderComponent: FC<{ + social: Array<{ identifier: string; name: string }>; + article: Array<{ identifier: string; name: string }>; +}> = (props) => { + const fetch = useFetch(); + const modal = useModals(); + const { social, article } = props; + const getSocialLink = useCallback( + (identifier: string) => async () => { + const { url } = await ( + await fetch('/integrations/social/' + identifier) + ).json(); + window.location.href = url; + }, + [] + ); + + const showApiButton = useCallback( + (identifier: string, name: string) => async () => { + modal.openModal({ + title: `Add ${name}`, + children: , + }); + }, + [] + ); + return ( +
+
+

Social

+
+ {social.map((item) => ( +
+ {item.name} +
+ ))} +
+
+
+

Articles

+
+ {article.map((item) => ( +
+ {item.name} +
+ ))} +
+
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index eba5bda6..5e1467d5 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -1,14 +1,23 @@ 'use client'; -import "reflect-metadata"; +import 'reflect-metadata'; import weekOfYear from 'dayjs/plugin/weekOfYear'; import isoWeek from 'dayjs/plugin/isoWeek'; import utc from 'dayjs/plugin/utc'; -import {createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import { + createContext, + FC, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import dayjs from 'dayjs'; -import useSWR from "swr"; -import {useFetch} from "@gitroom/helpers/utils/custom.fetch"; -import {Post, Integration} from '@prisma/client'; +import useSWR, { useSWRConfig } from 'swr'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Post, Integration } from '@prisma/client'; dayjs.extend(weekOfYear); dayjs.extend(isoWeek); @@ -16,9 +25,11 @@ dayjs.extend(utc); const CalendarContext = createContext({ currentWeek: dayjs().week(), + currentYear: dayjs().year(), + comments: [] as Array<{ date: string; total: number }>, integrations: [] as Integrations[], - posts: [] as Array, - setFilters: (filters: { currentWeek: number, currentYear: number }) => {}, + posts: [] as Array, + setFilters: (filters: { currentWeek: number; currentYear: number }) => {}, changeDate: (id: string, date: dayjs.Dayjs) => {}, }); @@ -29,51 +40,82 @@ export interface Integrations { type: string; picture: string; } -export const CalendarWeekProvider: FC<{ children: ReactNode, integrations: Integrations[] }> = ({ - children, - integrations -}) => { +export const CalendarWeekProvider: FC<{ + children: ReactNode; + integrations: Integrations[]; +}> = ({ children, integrations }) => { const fetch = useFetch(); const [internalData, setInternalData] = useState([] as any[]); + const { mutate } = useSWRConfig(); const [filters, setFilters] = useState({ - currentWeek: dayjs().week(), - currentYear: dayjs().year(), + currentWeek: dayjs().week(), + currentYear: dayjs().year(), }); + const setFiltersWrapper = useCallback( + (filters: { currentWeek: number; currentYear: number }) => { + setFilters(filters); + setTimeout(() => { + mutate('/posts'); + }); + }, + [filters] + ); + const params = useMemo(() => { return new URLSearchParams({ - week: filters.currentWeek.toString(), - year: filters.currentYear.toString(), + week: filters.currentWeek.toString(), + year: filters.currentYear.toString(), }).toString(); }, [filters]); - const loadData = useCallback(async(url: string) => { - return (await fetch(url)).json(); - }, [filters]); + const loadData = useCallback( + async (url: string) => { + return (await fetch(`${url}?${params}`)).json(); + }, + [filters] + ); - const {data, isLoading} = useSWR(`/posts?${params}`, loadData); + const swr = useSWR(`/posts`, loadData); + const { isLoading } = swr; + const { posts, comments } = swr?.data || { posts: [], comments: [] }; - const changeDate = useCallback((id: string, date: dayjs.Dayjs) => { - setInternalData(d => d.map((post: Post) => { - if (post.id === id) { - return {...post, publishDate: date.format('YYYY-MM-DDTHH:mm:ss')}; - } - return post; - })); - }, [data, internalData]); + console.log(comments); + const changeDate = useCallback( + (id: string, date: dayjs.Dayjs) => { + setInternalData((d) => + d.map((post: Post) => { + if (post.id === id) { + return { ...post, publishDate: date.format('YYYY-MM-DDTHH:mm:ss') }; + } + return post; + }) + ); + }, + [posts, internalData] + ); useEffect(() => { - if (data) { - setInternalData(data); + if (posts) { + setInternalData(posts); } - }, [data]); + }, [posts]); return ( - + {children} ); }; -export const useCalendar = () => useContext(CalendarContext); \ No newline at end of file +export const useCalendar = () => useContext(CalendarContext); diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index 90ea4bdc..8c85801a 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -6,7 +6,7 @@ import { useCalendar, } from '@gitroom/frontend/components/launches/calendar.context'; import dayjs from 'dayjs'; -import { useModals } from '@mantine/modals'; +import { openModal, useModals } from '@mantine/modals'; import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model'; import clsx from 'clsx'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; @@ -14,6 +14,9 @@ import { ExistingDataContextProvider } from '@gitroom/frontend/components/launch import { useDrag, useDrop } from 'react-dnd'; import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider'; import { Integration, Post } from '@prisma/client'; +import { useAddProvider } from '@gitroom/frontend/components/launches/add.provider.component'; +import { CommentComponent } from '@gitroom/frontend/components/launches/comments/comment.component'; +import { useSWRConfig } from 'swr'; const days = [ '', @@ -52,6 +55,235 @@ const hours = [ '23:00', ]; +export const Calendar = () => { + const { currentWeek, currentYear, comments } = useCalendar(); + + const firstDay = useMemo(() => { + return dayjs().year(currentYear).isoWeek(currentWeek).isoWeekday(1); + }, [currentYear, currentWeek]); + + return ( + +
+
+ {days.map((day, index) => ( +
+
{day}
+
+ {day && `(${firstDay.add(index - 1, 'day').format('DD/MM')})`} +
+
+ ))} + {hours.map((hour) => + days.map((day, index) => ( + <> + {index === 0 ? ( +
+ {['00', '10', '20', '30', '40', '50'].map((num) => ( +
+ {hour.split(':')[0] + ':' + num} +
+ ))} +
+ ) : ( +
+ + dayjs + .utc(p.date) + .local() + .format('YYYY-MM-DD HH:mm') === + dayjs() + .isoWeekday(index + 1) + .hour(+hour.split(':')[0] - 1) + .minute(0) + .format('YYYY-MM-DD HH:mm') + )?.total || 0 + } + date={dayjs() + .isoWeekday(index + 1) + .hour(+hour.split(':')[0] - 1) + .minute(0)} + /> + {['00', '10', '20', '30', '40', '50'].map((num) => ( + + ))} +
+ )} + + )) + )} +
+
+
+ ); +}; + +const CalendarColumn: FC<{ day: number; hour: string }> = (props) => { + const { day, hour } = props; + const { currentWeek, currentYear, integrations, posts, changeDate } = + useCalendar(); + const modal = useModals(); + const fetch = useFetch(); + + const getDate = useMemo(() => { + const date = + dayjs() + .year(currentYear) + .isoWeek(currentWeek) + .isoWeekday(day) + .format('YYYY-MM-DD') + + 'T' + + hour + + ':00'; + return dayjs(date); + }, [currentWeek]); + + const postList = useMemo(() => { + return posts.filter((post) => { + return dayjs(post.publishDate).local().isSame(getDate); + }); + }, [posts]); + + const isBeforeNow = useMemo(() => { + return getDate.isBefore(dayjs()); + }, [getDate]); + + const [{ canDrop }, drop] = useDrop(() => ({ + accept: 'post', + drop: (item: any) => { + if (isBeforeNow) return; + fetch(`/posts/${item.id}/date`, { + method: 'PUT', + body: JSON.stringify({ + date: getDate.utc().format('YYYY-MM-DDTHH:mm:ss'), + }), + }); + changeDate(item.id, getDate); + }, + collect: (monitor) => ({ + canDrop: isBeforeNow ? false : !!monitor.canDrop() && !!monitor.isOver(), + }), + })); + + const editPost = useCallback( + (id: string) => async () => { + const data = await (await fetch(`/posts/${id}`)).json(); + + modal.openModal({ + closeOnClickOutside: false, + closeOnEscape: false, + withCloseButton: false, + classNames: { + modal: 'bg-transparent text-white', + }, + children: ( + + f.id === data.integration + )} + date={getDate} + /> + + ), + size: '80%', + title: ``, + }); + }, + [] + ); + + const addModal = useCallback(() => { + modal.openModal({ + closeOnClickOutside: false, + closeOnEscape: false, + withCloseButton: false, + classNames: { + modal: 'bg-transparent text-white', + }, + children: , + size: '80%', + // title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`, + }); + }, []); + + const addProvider = useAddProvider(); + + return ( +
+
+
+ {postList.map((post) => ( +
1 && 'w-[33px] basis-[28px]', + 'h-full text-white relative flex justify-center items-center flex-grow-0 flex-shrink-0' + )} + > +
+ +
+
+ ))} + {!isBeforeNow && ( +
+
+ + +
+
+ )} +
+
+
+ ); +}; + const CalendarItem: FC<{ date: dayjs.Dayjs; editPost: () => void; @@ -98,187 +330,61 @@ const CalendarItem: FC<{ ); }; -const CalendarColumn: FC<{ day: number; hour: string }> = (props) => { - const { day, hour } = props; - const { currentWeek, integrations, posts, changeDate } = useCalendar(); - const modal = useModals(); - const fetch = useFetch(); +export const CommentBox: FC<{ totalComments: number; date: dayjs.Dayjs }> = ( + props +) => { + const { totalComments, date } = props; + const { mutate } = useSWRConfig(); - const getDate = useMemo(() => { - const date = - dayjs().isoWeek(currentWeek).isoWeekday(day).format('YYYY-MM-DD') + - 'T' + - hour + - ':00'; - return dayjs(date); - }, [currentWeek]); - - const postList = useMemo(() => { - return posts.filter((post) => { - return dayjs(post.publishDate).local().isSame(getDate); - }); - }, [posts]); - - const isBeforeNow = useMemo(() => { - return getDate.isBefore(dayjs()); - }, [getDate]); - - const [{ canDrop }, drop] = useDrop(() => ({ - accept: 'post', - drop: (item: any) => { - if (isBeforeNow) return; - fetch(`/posts/${item.id}/date`, { - method: 'PUT', - body: JSON.stringify({ date: getDate.utc().format('YYYY-MM-DDTHH:mm:ss') }), - }); - changeDate(item.id, getDate); - }, - collect: (monitor) => ({ - canDrop: isBeforeNow ? false : !!monitor.canDrop() && !!monitor.isOver(), - }), - })); - - const editPost = useCallback( - (id: string) => async () => { - const data = await (await fetch(`/posts/${id}`)).json(); - - modal.openModal({ - closeOnClickOutside: false, - closeOnEscape: false, - withCloseButton: false, - children: ( - - f.id === data.integration - )} - date={getDate} - /> - - ), - size: '80%', - title: `Edit post for ${getDate.format('DD/MM/YYYY HH:mm')}`, - }); - }, - [] - ); - - const addModal = useCallback(() => { - modal.openModal({ - closeOnClickOutside: false, - closeOnEscape: false, + const openCommentsModal = useCallback(() => { + openModal({ + children: , withCloseButton: false, - children: , + onClose() { + mutate(`/posts`); + }, + classNames: { + modal: 'bg-transparent text-white', + }, size: '80%', - title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`, }); - }, []); + }, [date]); return ( -
-
+
+
- {postList.map((post) => ( -
1 && 'w-[33px] basis-[28px]', - 'h-full text-white relative flex justify-center items-center flex-grow-0 flex-shrink-0' - )} - > -
- -
-
- ))} - {!isBeforeNow && ( -
-
- + -
+ {totalComments > 0 && ( +
+ {totalComments}
)} + + +
+
); }; - -export const Calendar = () => { - return ( - -
-
- {days.map((day) => ( -
- {day} -
- ))} - {hours.map((hour) => - days.map((day, index) => ( - <> - {index === 0 ? ( -
- {['00', '10', '20', '30', '40', '50'].map((num) => ( -
- {hour.split(':')[0] + ':' + num} -
- ))} -
- ) : ( -
- {['00', '10', '20', '30', '40', '50'].map((num) => ( - - ))} -
- )} - - )) - )} -
-
-
- ); -}; diff --git a/apps/frontend/src/components/launches/comments/comment.component.tsx b/apps/frontend/src/components/launches/comments/comment.component.tsx new file mode 100644 index 00000000..f964836d --- /dev/null +++ b/apps/frontend/src/components/launches/comments/comment.component.tsx @@ -0,0 +1,380 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import dayjs from 'dayjs'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { useModals } from '@mantine/modals'; +import { Textarea } from '@gitroom/react/form/textarea'; +import { Button } from '@gitroom/react/form/button'; +import clsx from 'clsx'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { Input } from '@gitroom/react/form/input'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; + +export const CommentBox: FC<{ + value?: string; + type: 'textarea' | 'input'; + onChange: (comment: string) => void; +}> = (props) => { + const { value, onChange, type } = props; + const Component = type === 'textarea' ? Textarea : Input; + const [newComment, setNewComment] = useState(value || ''); + + const newCommentFunc = useCallback( + (event: { target: { value: string } }) => { + setNewComment(event.target.value); + }, + [newComment] + ); + + const changeIt = useCallback(() => { + onChange(newComment); + setNewComment(''); + }, [newComment]); + + return ( +
+
+ +
+ +
+ ); +}; + +interface Comments { + id: string; + content: string; + user: { email: string; id: string }; + childrenComment: Comments[]; +} + +export const EditableCommentComponent: FC<{ + comment: Comments; + onEdit: (content: string) => void; + onDelete: () => void; +}> = (props) => { + const { comment, onEdit, onDelete } = props; + const [commentContent, setCommentContent] = useState(comment.content); + const [editMode, setEditMode] = useState(false); + const user = useUser(); + + const updateComment = useCallback((commentValue: string) => { + if (commentValue !== comment.content) { + setCommentContent(commentValue); + onEdit(commentValue); + } + setEditMode(false); + }, []); + + const deleteCommentFunction = useCallback(async () => { + if ( + await deleteDialog( + 'Are you sure you want to delete this comment?', + 'Yes, Delete' + ) + ) { + onDelete(); + } + }, []); + + if (editMode) { + return ( + + ); + } + + return ( +
+
{commentContent}
+ {user?.id === comment.user.id && ( + <> + setEditMode(!editMode)} + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 32 32" + fill="none" + > + + + + + + + + )} +
+ ); +}; + +export const CommentComponent: FC<{ date: dayjs.Dayjs }> = (props) => { + const { date } = props; + const { closeAll } = useModals(); + const [commentsList, setCommentsList] = useState([]); + const user = useUser(); + const fetch = useFetch(); + + const load = useCallback(async () => { + const data = await ( + await fetch(`/comments/${date.utc().format('YYYY-MM-DDTHH:mm:00')}`) + ).json(); + + setCommentsList(data); + }, []); + + useEffect(() => { + load(); + }, []); + + const editComment = useCallback( + (comment: Comments) => async (content: string) => { + fetch(`/comments/${comment.id}`, { + method: 'PUT', + body: JSON.stringify({ + content, + date: date.utc().format('YYYY-MM-DDTHH:mm:00'), + }), + }); + }, + [] + ); + + const addComment = useCallback( + async (content: string) => { + const { id } = await ( + await fetch('/comments', { + method: 'POST', + body: JSON.stringify({ + content, + date: date.utc().format('YYYY-MM-DDTHH:mm:00'), + }), + }) + ).json(); + + setCommentsList(list => ([ + { + id, + user: { email: user?.email!, id: user?.id! }, + content, + childrenComment: [], + }, + ...list, + ])); + }, + [commentsList, setCommentsList] + ); + + const deleteComment = useCallback( + (comment: Comments) => async () => { + await fetch(`/comments/${comment.id}`, { + method: 'DELETE', + }); + setCommentsList((list) => list.filter((item) => item.id !== comment.id)); + }, + [commentsList, setCommentsList] + ); + + const deleteChildrenComment = useCallback( + (parent: Comments, children: Comments) => async () => { + await fetch(`/comments/${children.id}`, { + method: 'DELETE', + }); + + setCommentsList((list) => + list.map((item) => { + if (item.id === parent.id) { + return { + ...item, + childrenComment: item.childrenComment.filter( + (child) => child.id !== children.id + ), + }; + } + return item; + }) + ); + }, + [commentsList, setCommentsList] + ); + + const addChildrenComment = useCallback( + (comment: Comments) => async (content: string) => { + const { id } = await ( + await fetch(`/comments/${comment.id}`, { + method: 'POST', + body: JSON.stringify({ + content, + date: date.utc().format('YYYY-MM-DDTHH:mm:00'), + }), + }) + ).json(); + + setCommentsList((list) => + list.map((item) => { + if (item.id === comment.id) { + return { + ...item, + childrenComment: [ + ...item.childrenComment, + { + id, + user: { email: user?.email!, id: user?.id! }, + content, + childrenComment: [], + }, + ], + }; + } + return item; + }) + ); + }, + [commentsList] + ); + + const extractNameFromEmailAndCapitalize = useCallback((email: string) => { + return ( + email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1) + ); + }, []); + + return ( +
+ + + + + +
+ {commentsList.map((comment, index) => ( + <> +
+
+
+
+ {comment.user.email[0].toUpperCase()} +
+
+
+
+
+
+ {extractNameFromEmailAndCapitalize(comment.user.email)} +
+
+ +
+
+ +
+ {comment?.childrenComment?.map((childComment, index2) => ( +
+
+
+ {childComment.user.email[0].toUpperCase()} +
+
+
+
+
+ {extractNameFromEmailAndCapitalize( + childComment.user.email + )} +
+
+ +
+
+ ))} +
+
+
+
+
+
+
+
+ +
+
+ + ))} +
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/filters.tsx b/apps/frontend/src/components/launches/filters.tsx index 569de78c..923283bd 100644 --- a/apps/frontend/src/components/launches/filters.tsx +++ b/apps/frontend/src/components/launches/filters.tsx @@ -1,9 +1,61 @@ -"use client"; -import {useCalendar} from "@gitroom/frontend/components/launches/calendar.context"; -import dayjs from "dayjs"; +'use client'; +import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context'; +import dayjs from 'dayjs'; +import {useCallback} from "react"; export const Filters = () => { const week = useCalendar(); - const betweenDates = dayjs().isoWeek(week.currentWeek).startOf('isoWeek').format('DD/MM/YYYY') + ' - ' + dayjs().isoWeek(week.currentWeek).endOf('isoWeek').format('DD/MM/YYYY'); - return
week.setFilters({currentWeek: week.currentWeek + 1})}>Week {week.currentWeek} ({betweenDates})
; + const betweenDates = + dayjs().year(week.currentYear).isoWeek(week.currentWeek).startOf('isoWeek').format('DD/MM/YYYY') + + ' - ' + + dayjs().year(week.currentYear).isoWeek(week.currentWeek).endOf('isoWeek').format('DD/MM/YYYY'); + + const nextWeek = useCallback(() => { + week.setFilters({ + currentWeek: week.currentWeek === 52 ? 1 : week.currentWeek + 1, + currentYear: week.currentWeek === 52 ? week.currentYear + 1 : week.currentYear, + }); + }, [week.currentWeek, week.currentYear]); + + const previousWeek = useCallback(() => { + week.setFilters({ + currentWeek: week.currentWeek === 1 ? 52 : week.currentWeek - 1, + currentYear: week.currentWeek === 1 ? week.currentYear - 1 : week.currentYear, + }); + }, [week.currentWeek, week.currentYear]); + + return ( +
+
+ + + +
+
Week {week.currentWeek}
+
+ + + +
+
{betweenDates}
+
+ ); }; diff --git a/apps/frontend/src/components/launches/helpers/new.image.component.tsx b/apps/frontend/src/components/launches/helpers/new.image.component.tsx new file mode 100644 index 00000000..85bacfe5 --- /dev/null +++ b/apps/frontend/src/components/launches/helpers/new.image.component.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { + executeCommand, + ExecuteState, + ICommand, + selectWord, + TextAreaTextApi, +} from '@uiw/react-md-editor'; +import { showMediaBox } from '@gitroom/frontend/components/media/media.component'; + +export const newImage: ICommand = { + name: 'image', + keyCommand: 'image', + shortcuts: 'ctrlcmd+k', + prefix: '![image](', + suffix: ')', + buttonProps: { + 'aria-label': 'Add image (ctrl + k)', + title: 'Add image (ctrl + k)', + }, + icon: ( + + + + ), + execute: (state: ExecuteState, api: TextAreaTextApi) => { + let newSelectionRange = selectWord({ + text: state.text, + selection: state.selection, + prefix: state.command.prefix!, + suffix: state.command.suffix, + }); + + let state1 = api.setSelectionRange(newSelectionRange); + + if ( + state1.selectedText.includes('http') || + state1.selectedText.includes('www') + ) { + executeCommand({ + api, + selectedText: state1.selectedText, + selection: state.selection, + prefix: state.command.prefix!, + suffix: state.command.suffix, + }); + + return ; + } + + newSelectionRange = selectWord({ + text: state.text, + selection: state.selection, + prefix: '![', + suffix: ']()', + }); + state1 = api.setSelectionRange(newSelectionRange); + + showMediaBox((media) => { + if (media) { + if (state1.selectedText.length > 0) { + executeCommand({ + api, + selectedText: state1.selectedText, + selection: state.selection, + prefix: '![', + suffix: `](${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${media.path})`, + }); + + return; + } + + executeCommand({ + api, + selectedText: state1.selectedText, + selection: state.selection, + prefix: '![image', + suffix: `](${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${media.path})`, + }); + } + }); + }, +}; diff --git a/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx b/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx new file mode 100644 index 00000000..481ca175 --- /dev/null +++ b/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx @@ -0,0 +1,234 @@ +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; +import { useMoveToIntegrationListener } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import clsx from 'clsx'; +import Image from 'next/image'; + +export const PickPlatforms: FC<{ + integrations: Integrations[]; + selectedIntegrations: Integrations[]; + onChange: (integrations: Integrations[]) => void; + singleSelect: boolean; + hide?: boolean; +}> = (props) => { + const { hide, integrations, selectedIntegrations, onChange } = props; + const ref = useRef(null); + + const [isLeft, setIsLeft] = useState(false); + const [isRight, setIsRight] = useState(false); + + const [selectedAccounts, setSelectedAccounts] = + useState(selectedIntegrations); + + useEffect(() => { + if ( + props.singleSelect && + selectedAccounts.length && + integrations.indexOf(selectedAccounts?.[0]) === -1 + ) { + addPlatform(integrations[0])(); + } + }, [integrations, selectedAccounts]); + + const checkLeftRight = (test = true) => { + const scrollWidth = ref.current?.scrollWidth; + const offsetWidth = +(ref.current?.offsetWidth || 0); + const scrollOffset = ref.current?.scrollLeft || 0; + + const totalScroll = scrollOffset + offsetWidth + 100; + + setIsLeft(!!scrollOffset); + setIsRight(!!scrollWidth && !!offsetWidth && scrollWidth > totalScroll); + }; + + const changeOffset = useCallback( + (offset: number) => () => { + if (ref.current) { + ref.current.scrollLeft += offset; + checkLeftRight(); + } + }, + [selectedIntegrations, integrations, selectedAccounts] + ); + + useEffect(() => { + checkLeftRight(); + }, [selectedIntegrations, integrations]); + + useMoveToIntegrationListener([integrations], props.singleSelect, (identifier) => { + const findIntegration = integrations.find( + (p) => p.id === identifier + ); + + if (findIntegration) { + addPlatform(findIntegration)(); + } + }); + + const addPlatform = useCallback( + (integration: Integrations) => async () => { + if (props.singleSelect) { + onChange([integration]); + setSelectedAccounts([integration]); + return; + } + if (selectedAccounts.includes(integration)) { + const changedIntegrations = selectedAccounts.filter( + ({ id }) => id !== integration.id + ); + + if ( + !props.singleSelect && + !(await deleteDialog( + 'Are you sure you want to remove this platform?' + )) + ) { + return; + } + onChange(changedIntegrations); + setSelectedAccounts(changedIntegrations); + } else { + const changedIntegrations = [...selectedAccounts, integration]; + onChange(changedIntegrations); + setSelectedAccounts(changedIntegrations); + } + }, + [selectedAccounts] + ); + + if (hide) { + return null; + } + + return ( +
+ {props.singleSelect && ( +
+ {isLeft && ( + + + + )} +
+ )} +
+
+
+
+ {integrations.map((integration) => + !props.singleSelect ? ( +
+
p.id === integration.id + ) === -1 + ? 'opacity-40' + : '' + )} + > + {integration.identifier} + {integration.identifier} +
+
+ ) : ( +
+
p.id === integration.id + ) === -1 + ? 'bg-third border border-third' + : 'bg-[#291259] border border-[#5826C2]' + )} + > +
+
+ {integration.identifier} + {integration.identifier} +
+
{integration.name}
+
+
+
+ ) + )} +
+
+
+
+ {props.singleSelect && isRight && ( +
+ + + +
+ )} +
+ ); +}; diff --git a/apps/frontend/src/components/launches/helpers/top.title.component.tsx b/apps/frontend/src/components/launches/helpers/top.title.component.tsx new file mode 100644 index 00000000..9c6cdcc8 --- /dev/null +++ b/apps/frontend/src/components/launches/helpers/top.title.component.tsx @@ -0,0 +1,49 @@ +import {FC} from "react"; + +export const TopTitle: FC<{ + title: string; + shouldExpend?: boolean; + expend?: () => void; + collapse?: () => void; +}> = (props) => { + const { title, shouldExpend, expend, collapse } = props; + + return ( +
+
{title}
+ {shouldExpend !== undefined && ( +
+ {!shouldExpend ? ( + + + + ) : ( + + + + )} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts b/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts index c71c4c40..8b903436 100644 --- a/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts +++ b/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts @@ -6,7 +6,7 @@ export const useCustomProviderFunction = () => { const { integration } = useIntegration(); const fetch = useFetch(); const get = useCallback( - async (funcName: string, customData?: string) => { + async (funcName: string, customData?: any) => { return ( await fetch('/integrations/function', { method: 'POST', diff --git a/apps/frontend/src/components/launches/helpers/use.existing.data.tsx b/apps/frontend/src/components/launches/helpers/use.existing.data.tsx index 4c7d7ad0..26c2e764 100644 --- a/apps/frontend/src/components/launches/helpers/use.existing.data.tsx +++ b/apps/frontend/src/components/launches/helpers/use.existing.data.tsx @@ -3,6 +3,7 @@ import {Post} from "@prisma/client"; const ExistingDataContext = createContext({ integration: '', + group: undefined as undefined | string, posts: [] as Post[], settings: {} as any }); diff --git a/apps/frontend/src/components/launches/helpers/use.expend.tsx b/apps/frontend/src/components/launches/helpers/use.expend.tsx new file mode 100644 index 00000000..b17b3536 --- /dev/null +++ b/apps/frontend/src/components/launches/helpers/use.expend.tsx @@ -0,0 +1,34 @@ +"use client"; + +import EventEmitter from 'events'; +import {useEffect, useState} from "react"; + +const emitter = new EventEmitter(); + +export const useExpend = () => { + const [expend, setExpend] = useState(false); + useEffect(() => { + const hide = () => { + setExpend(false); + }; + const show = () => { + setExpend(true); + }; + emitter.on('hide', hide); + emitter.on('show', show); + return () => { + emitter.off('hide', hide); + emitter.off('show', show); + }; + }, []); + + return { + expend, + hide: () => { + emitter.emit('hide'); + }, + show: () => { + emitter.emit('show'); + } + } +} diff --git a/apps/frontend/src/components/launches/helpers/use.formatting.ts b/apps/frontend/src/components/launches/helpers/use.formatting.ts index 61e94d9a..5f2cc553 100644 --- a/apps/frontend/src/components/launches/helpers/use.formatting.ts +++ b/apps/frontend/src/components/launches/helpers/use.formatting.ts @@ -1,31 +1,42 @@ -import removeMd from "remove-markdown"; -import {useMemo} from "react"; +import removeMd from 'remove-markdown'; +import { useMemo } from 'react'; -export const useFormatting = (text: Array<{content: string, id?: string}>, params: { - removeMarkdown?: boolean, - saveBreaklines?: boolean, - specialFunc?: (text: string) => string, -}) => { - return useMemo(() => { - return text.map((value) => { - let newText = value.content; - if (params.saveBreaklines) { - newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'); - } - if (params.removeMarkdown) { - newText = removeMd(value.content); - } - if (params.saveBreaklines) { - newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'); - } - if (params.specialFunc) { - newText = params.specialFunc(newText); - } - return { - id: value.id, - text: newText, - count: params.removeMarkdown && params.saveBreaklines ? newText.replace(/\n/g, ' ').length : newText.length, - } - }); - }, [text]); -} \ No newline at end of file +export const useFormatting = ( + text: Array<{ + content: string; + image?: Array<{ id: string; path: string }>; + id?: string; + }>, + params: { + removeMarkdown?: boolean; + saveBreaklines?: boolean; + specialFunc?: (text: string) => string; + } +) => { + return useMemo(() => { + return text.map((value) => { + let newText = value.content; + if (params.saveBreaklines) { + newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'); + } + if (params.removeMarkdown) { + newText = removeMd(value.content); + } + if (params.saveBreaklines) { + newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'); + } + if (params.specialFunc) { + newText = params.specialFunc(newText); + } + return { + id: value.id, + text: newText, + images: value.image, + count: + params.removeMarkdown && params.saveBreaklines + ? newText.replace(/\n/g, ' ').length + : newText.length, + }; + }); + }, [text]); +}; diff --git a/apps/frontend/src/components/launches/helpers/use.integration.ts b/apps/frontend/src/components/launches/helpers/use.integration.ts index 36e31022..bd9cb275 100644 --- a/apps/frontend/src/components/launches/helpers/use.integration.ts +++ b/apps/frontend/src/components/launches/helpers/use.integration.ts @@ -3,6 +3,6 @@ import {createContext, useContext} from "react"; import {Integrations} from "@gitroom/frontend/components/launches/calendar.context"; -export const IntegrationContext = createContext<{integration: Integrations|undefined, value: Array<{content: string, id?: string}>}>({integration: undefined, value: []}); +export const IntegrationContext = createContext<{integration: Integrations|undefined, value: Array<{content: string, id?: string, image?: Array<{path: string, id: string}>}>}>({integration: undefined, value: []}); export const useIntegration = () => useContext(IntegrationContext); \ No newline at end of file diff --git a/apps/frontend/src/components/launches/helpers/use.move.to.integration.tsx b/apps/frontend/src/components/launches/helpers/use.move.to.integration.tsx index ba4d0f2f..f277a976 100644 --- a/apps/frontend/src/components/launches/helpers/use.move.to.integration.tsx +++ b/apps/frontend/src/components/launches/helpers/use.move.to.integration.tsx @@ -1,7 +1,7 @@ 'use client'; import EventEmitter from 'events'; -import {useCallback, useEffect} from 'react'; +import { useCallback, useEffect } from 'react'; const emitter = new EventEmitter(); export const useMoveToIntegration = () => { @@ -11,6 +11,7 @@ export const useMoveToIntegration = () => { }; export const useMoveToIntegrationListener = ( + useEffectParams: any[], enabled: boolean, callback: (identifier: string) => void ) => { @@ -19,12 +20,13 @@ export const useMoveToIntegrationListener = ( return; } return load(); - }, []); + }, useEffectParams); const load = useCallback(() => { + emitter.off('moveToIntegration', callback); emitter.on('moveToIntegration', callback); return () => { emitter.off('moveToIntegration', callback); }; - }, []); + }, useEffectParams); }; diff --git a/apps/frontend/src/components/launches/helpers/use.values.ts b/apps/frontend/src/components/launches/helpers/use.values.ts index baa2fdeb..34ab1a50 100644 --- a/apps/frontend/src/components/launches/helpers/use.values.ts +++ b/apps/frontend/src/components/launches/helpers/use.values.ts @@ -3,7 +3,7 @@ import {useForm, useFormContext} from 'react-hook-form'; import {classValidatorResolver} from "@hookform/resolvers/class-validator"; const finalInformation = {} as { - [key: string]: { posts: Array<{id?: string, content: string, media?: Array}>; settings: () => object; isValid: boolean }; + [key: string]: { posts: Array<{id?: string, content: string, media?: Array}>; settings: () => object; trigger: () => Promise; isValid: boolean }; }; export const useValues = (initialValues: object, integration: string, identifier: string, value: Array<{id?: string, content: string, media?: Array}>, dto: any) => { const resolver = useMemo(() => { @@ -13,7 +13,8 @@ export const useValues = (initialValues: object, integration: string, identifier const form = useForm({ resolver, values: initialValues, - mode: 'onChange' + mode: 'onChange', + criteriaMode: 'all', }); const getValues = useMemo(() => { @@ -24,6 +25,7 @@ export const useValues = (initialValues: object, integration: string, identifier finalInformation[integration].posts = value; finalInformation[integration].isValid = form.formState.isValid; finalInformation[integration].settings = getValues; + finalInformation[integration].trigger = form.trigger; useEffect(() => { return () => { diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index c7fedd3b..c7fe5843 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -3,11 +3,14 @@ import { FC, useMemo } from 'react'; import Image from 'next/image'; import { orderBy } from 'lodash'; import { Calendar } from '@gitroom/frontend/components/launches/calendar'; -import {CalendarWeekProvider, Integrations} from '@gitroom/frontend/components/launches/calendar.context'; +import { + CalendarWeekProvider, + Integrations, +} from '@gitroom/frontend/components/launches/calendar.context'; import { Filters } from '@gitroom/frontend/components/launches/filters'; export const LaunchesComponent: FC<{ - integrations: Integrations[] + integrations: Integrations[]; }> = (props) => { const { integrations } = props; @@ -18,12 +21,14 @@ export const LaunchesComponent: FC<{ return (
-

Channels

+ {sortedIntegrations.length === 0 && ( +
No channels
+ )} {sortedIntegrations.map((integration) => (
{integration.name}
-
3
))}
+
diff --git a/apps/frontend/src/components/launches/providers.options.tsx b/apps/frontend/src/components/launches/providers.options.tsx new file mode 100644 index 00000000..05ce9034 --- /dev/null +++ b/apps/frontend/src/components/launches/providers.options.tsx @@ -0,0 +1,41 @@ +import {FC, useEffect, useState} from "react"; +import {Integrations} from "@gitroom/frontend/components/launches/calendar.context"; +import {PickPlatforms} from "@gitroom/frontend/components/launches/helpers/pick.platform.component"; +import {IntegrationContext} from "@gitroom/frontend/components/launches/helpers/use.integration"; +import {ShowAllProviders} from "@gitroom/frontend/components/launches/providers/show.all.providers"; + +export const ProvidersOptions: FC<{ + integrations: Integrations[]; + editorValue: Array<{ id?: string; content: string }>; +}> = (props) => { + const { integrations, editorValue } = props; + const [selectedIntegrations, setSelectedIntegrations] = useState([ + integrations[0], + ]); + + useEffect(() => { + if (integrations.indexOf(selectedIntegrations[0]) === -1) { + setSelectedIntegrations([integrations[0]]); + } + }, [integrations, selectedIntegrations]); + return ( +
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx b/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx index 6ad8e930..457b3fa8 100644 --- a/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx +++ b/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx @@ -1,31 +1,85 @@ -import {FC} from "react"; -import {withProvider} from "@gitroom/frontend/components/launches/providers/high.order.provider"; -import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto"; -import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values"; -import {Input} from "@gitroom/react/form/input"; -import {MediaComponent} from "@gitroom/frontend/components/media/media.component"; -import {SelectOrganization} from "@gitroom/frontend/components/launches/providers/devto/select.organization"; -import {DevtoTags} from "@gitroom/frontend/components/launches/providers/devto/devto.tags"; +import { FC } from 'react'; +import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { Input } from '@gitroom/react/form/input'; +import { MediaComponent } from '@gitroom/frontend/components/media/media.component'; +import { SelectOrganization } from '@gitroom/frontend/components/launches/providers/devto/select.organization'; +import { DevtoTags } from '@gitroom/frontend/components/launches/providers/devto/devto.tags'; +import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; +import clsx from 'clsx'; +import localFont from 'next/font/local'; +import MDEditor from '@uiw/react-md-editor'; +import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; + +const font = localFont({ + src: [ + { + path: './fonts/SFNS.woff2', + }, + ], +}); const DevtoPreview: FC = () => { - return
asd
+ const { value} = useIntegration(); + const settings = useSettings(); + const image = useMediaDirectory(); + const [coverPicture, title, tags] = settings.watch([ + 'main_image', + 'title', + 'tags', + ]); + + return ( +
+ {!!coverPicture?.path && ( +
+ cover_picture +
+ )} +
+
{title}
+
+ {tags?.map((p: any) => ( +
#{p.label}
+ ))} +
+
+
+ p.content).join('\n')} /> +
+
+ ); }; const DevtoSettings: FC = () => { - const form = useSettings(); - return ( - <> - - - -
- -
-
- -
- - ) + const form = useSettings(); + return ( + <> + + + +
+ +
+
+ +
+ + ); }; -export default withProvider(DevtoSettings, DevtoPreview, DevToSettingsDto); \ No newline at end of file +export default withProvider(DevtoSettings, DevtoPreview, DevToSettingsDto); diff --git a/apps/frontend/src/components/launches/providers/devto/fonts/SFNS.woff2 b/apps/frontend/src/components/launches/providers/devto/fonts/SFNS.woff2 new file mode 100644 index 00000000..c9d0228f Binary files /dev/null and b/apps/frontend/src/components/launches/providers/devto/fonts/SFNS.woff2 differ diff --git a/apps/frontend/src/components/launches/providers/hashnode/hashnode.provider.tsx b/apps/frontend/src/components/launches/providers/hashnode/hashnode.provider.tsx new file mode 100644 index 00000000..893ccb92 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/hashnode/hashnode.provider.tsx @@ -0,0 +1,87 @@ +import { FC } from 'react'; +import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { Input } from '@gitroom/react/form/input'; +import { HashnodePublications } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.publications'; +import { HashnodeTags } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.tags'; +import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto'; +import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; +import clsx from 'clsx'; +import MDEditor from '@uiw/react-md-editor'; +import { Plus_Jakarta_Sans } from 'next/font/google'; +import { MediaComponent } from '@gitroom/frontend/components/media/media.component'; + +const font = Plus_Jakarta_Sans({ + subsets: ['latin'], +}); + +const HashnodePreview: FC = () => { + const { value } = useIntegration(); + const settings = useSettings(); + const image = useMediaDirectory(); + const [coverPicture, title, subtitle] = settings.watch([ + 'main_image', + 'title', + 'subtitle', + ]); + + return ( +
+ {!!coverPicture?.path && ( +
+ cover_picture +
+ )} +
+
{title}
+
{subtitle}
+
+
+ p.content).join('\n')} + /> +
+
+ ); +}; + +const HashnodeSettings: FC = () => { + const form = useSettings(); + return ( + <> + + + + +
+ +
+
+ +
+ + ); +}; + +export default withProvider( + HashnodeSettings, + HashnodePreview, + HashnodeSettingsDto +); diff --git a/apps/frontend/src/components/launches/providers/hashnode/hashnode.publications.tsx b/apps/frontend/src/components/launches/providers/hashnode/hashnode.publications.tsx new file mode 100644 index 00000000..39c58e09 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/hashnode/hashnode.publications.tsx @@ -0,0 +1,44 @@ +import { FC, useEffect, useState } from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { Select } from '@gitroom/react/form/select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; + +export const HashnodePublications: FC<{ + name: string; + onChange: (event: { target: { value: string; name: string } }) => void; +}> = (props) => { + const { onChange, name } = props; + const customFunc = useCustomProviderFunction(); + const [publications, setOrgs] = useState([]); + const { getValues } = useSettings(); + const [currentMedia, setCurrentMedia] = useState(); + + const onChangeInner = (event: { target: { value: string, name: string } }) => { + setCurrentMedia(event.target.value); + onChange(event); + }; + + useEffect(() => { + customFunc.get('publications').then((data) => setOrgs(data)); + const settings = getValues()[props.name]; + if (settings) { + setCurrentMedia(settings); + } + }, []); + + + if (!publications.length) { + return null; + } + + return ( + + ); +}; diff --git a/apps/frontend/src/components/launches/providers/hashnode/hashnode.tags.tsx b/apps/frontend/src/components/launches/providers/hashnode/hashnode.tags.tsx new file mode 100644 index 00000000..244d490c --- /dev/null +++ b/apps/frontend/src/components/launches/providers/hashnode/hashnode.tags.tsx @@ -0,0 +1,67 @@ +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { ReactTags } from 'react-tag-autocomplete'; + +export const HashnodeTags: FC<{ + name: string; + label: string; + onChange: (event: { target: { value: any[]; name: string } }) => void; +}> = (props) => { + const { onChange, name, label } = props; + const customFunc = useCustomProviderFunction(); + const [tags, setTags] = useState([]); + const { getValues, formState: form } = useSettings(); + const [tagValue, setTagValue] = useState([]); + + 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 >= 4) { + return; + } + const modify = [...tagValue, newTag]; + setTagValue(modify); + onChange({ target: { value: modify, name } }); + }, + [tagValue] + ); + + useEffect(() => { + customFunc.get('tags').then((data) => setTags(data)); + const settings = getValues()[props.name]; + if (settings) { + setTagValue(settings); + } + }, []); + + const err = useMemo(() => { + if (!form || !form.errors[props?.name!]) return; + return form?.errors?.[props?.name!]?.message! as string; + }, [form?.errors?.[props?.name!]?.message]); + + if (!tags.length) { + return null; + } + + return ( +
+
{label}
+ +
{err || <> }
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/providers/high.order.provider.tsx b/apps/frontend/src/components/launches/providers/high.order.provider.tsx index 128e51d0..0e55784a 100644 --- a/apps/frontend/src/components/launches/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/launches/providers/high.order.provider.tsx @@ -1,9 +1,16 @@ 'use client'; -import { FC, useCallback, useEffect, useState } from 'react'; +import { + FC, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { Button } from '@gitroom/react/form/button'; import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; -import MDEditor from '@uiw/react-md-editor'; +import MDEditor, { commands } from '@uiw/react-md-editor'; import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor'; import { useValues } from '@gitroom/frontend/components/launches/helpers/use.values'; import { FormProvider } from 'react-hook-form'; @@ -13,46 +20,87 @@ import { IntegrationContext, useIntegration, } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; +import { createPortal } from 'react-dom'; +import clsx from 'clsx'; +import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component'; -// This is a simple function that if we edit in place, we hide the editor on top -export const EditorWrapper: FC = (props) => { - const showHide = useHideTopEditor(); +// Simple component to change back to settings on after changing tab +export const SetTab: FC<{ changeTab: () => void }> = (props) => { useEffect(() => { - showHide.hide(); return () => { - showHide.show(); + setTimeout(() => { + props.changeTab(); + }, 500); }; }, []); - return null; }; +// This is a simple function that if we edit in place, we hide the editor on top +export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => { + const showHide = useHideTopEditor(); + const [showEditor, setShowEditor] = useState(false); + + useEffect(() => { + setShowEditor(true); + showHide.hide(); + return () => { + showHide.show(); + setShowEditor(false); + }; + }, []); + + if (!showEditor) { + return null; + } + + return children; +}; + export const withProvider = ( - SettingsComponent: FC, + SettingsComponent: FC | null, PreviewComponent: FC, dto?: any ) => { return (props: { identifier: string; id: string; - value: Array<{ content: string; id?: string }>; + value: Array<{ + content: string; + id?: string; + image?: Array<{ path: string; id: string }>; + }>; show: boolean; }) => { const existingData = useExistingData(); const { integration } = useIntegration(); const [editInPlace, setEditInPlace] = useState(!!existingData.integration); const [InPlaceValue, setInPlaceValue] = useState< - Array<{ id?: string; content: string }> + Array<{ + id?: string; + content: string; + image?: Array<{ id: string; path: string }>; + }> >( + // @ts-ignore existingData.integration - ? existingData.posts.map((p) => ({ id: p.id, content: p.content })) + ? existingData.posts.map((p) => ({ + id: p.id, + content: p.content, + image: p.image, + })) : [{ content: '' }] ); - const [showTab, setShowTab] = useState(existingData.integration ? 1 : 0); + const [showTab, setShowTab] = useState(0); + + const Component = useMemo(() => { + return SettingsComponent ? SettingsComponent : () => <>; + }, [SettingsComponent]); // in case there is an error on submit, we change to the settings tab for the specific provider - useMoveToIntegrationListener(true, (identifier) => { - if (identifier === props.identifier) { + useMoveToIntegrationListener([props.id], true, (identifier) => { + if (identifier === props.id) { setShowTab(2); } }); @@ -77,6 +125,19 @@ export const withProvider = ( [InPlaceValue] ); + const changeImage = useCallback( + (index: number) => + (newValue: { + target: { name: string; value?: Array<{ id: string; path: string }> }; + }) => { + return setInPlaceValue((prev) => { + prev[index].image = newValue.target.value; + return [...prev]; + }); + }, + [InPlaceValue] + ); + // add another local editor const addValue = useCallback( (index: number) => () => { @@ -88,30 +149,46 @@ export const withProvider = ( [InPlaceValue] ); - // This is a function if we want to switch from the global editor to edit in place - const changeToEditor = useCallback( - (editor: boolean) => async () => { + // Delete post + const deletePost = useCallback( + (index: number) => async () => { if ( - editor && - !editInPlace && !(await deleteDialog( - 'Are you sure you want to edit in place?', - 'Yes, edit in place!' + 'Are you sure you want to delete this post?', + 'Yes, delete it!' )) ) { - return false; - } - setShowTab(editor ? 1 : 0); - if (editor && !editInPlace) { - setEditInPlace(true); - setInPlaceValue( - props.value.map((p) => ({ id: p.id, content: p.content })) - ); + return; } + setInPlaceValue((prev) => { + prev.splice(index, 1); + return [...prev]; + }); }, - [props.value, editInPlace] + [InPlaceValue] ); + // This is a function if we want to switch from the global editor to edit in place + const changeToEditor = useCallback(async () => { + if ( + !(await deleteDialog( + !editInPlace + ? 'Are you sure you want to edit only this?' + : 'Are you sure you want to revert it back to global editing?', + 'Yes, edit in place!' + )) + ) { + return false; + } + + setEditInPlace(!editInPlace); + setInPlaceValue( + editInPlace + ? [{ content: '' }] + : props.value.map((p) => ({ id: p.id, content: p.content })) + ); + }, [props.value, editInPlace]); + // this is a trick to prevent the data from being deleted, yet we don't render the elements if (!props.show) { return null; @@ -119,58 +196,161 @@ export const withProvider = ( return ( -
- {editInPlace && } -
-
-
-
- -
-
- +
+ )} +
+
- {showTab === 1 && ( -
- {InPlaceValue.map((val, index) => ( - <> - 1 ? 200 : 500} - value={val.content} - preview="edit" - // @ts-ignore - onChange={changeValue(index)} - /> -
- -
- - ))} -
- )} + {editInPlace && + createPortal( + +
+ {!existingData?.integration && ( +
+ This will edit only this provider +
+ )} + {InPlaceValue.map((val, index) => ( + <> +
+ 1 ? 200 : 250} + value={val.content} + commands={[ + ...commands + .getCommands() + .filter((f) => f.name !== 'image'), + newImage, + ]} + preview="edit" + // @ts-ignore + onChange={changeValue(index)} + /> + {(!val.content || val.content.length < 6) && ( +
+ The post should be at least 6 characters long +
+ )} +
+
+ +
+
+ {InPlaceValue.length > 1 && ( +
+
+ + + +
+
+ Delete Post +
+
+ )} +
+
+
+
+ +
+ + ))} +
+
, + document.querySelector('#renderEditor')! + )} {showTab === 2 && (
- +
)} {showTab === 0 && ( - - - +
+ + {(editInPlace ? InPlaceValue : props.value) + .map((p) => p.content) + .join('').length ? ( + + ) : ( + <>No Content Yet + )} + +
)}
diff --git a/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx b/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx index c8d84716..cd40533f 100644 --- a/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx +++ b/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx @@ -1,11 +1,12 @@ import { FC } from 'react'; import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; -import clsx from 'clsx'; import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting'; +import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; const LinkedinPreview: FC = (props) => { const { value: topValue, integration } = useIntegration(); + const mediaDir = useMediaDirectory(); const newValues = useFormatting(topValue, { removeMarkdown: true, saveBreaklines: true, @@ -14,59 +15,91 @@ const LinkedinPreview: FC = (props) => { }, }); + const [firstPost, ...morePosts] = newValues; + if (!firstPost) { + return null; + } + return ( -
-
- {newValues.map((value, index) => ( -
-
- x - {index !== topValue.length - 1 && ( -
- )} -
-
-
-
- {integration?.name} -
-
- - - - - -
-
- @username -
-
-
{value.text}
-
+
+
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom
- ))} +
1m
+
+
+
+          {firstPost?.text}
+        
+ + {!!firstPost?.images?.length && ( +
+ {firstPost.images.map((image, index) => ( + + + + ))} +
+ )} +
+ {morePosts.map((p, index) => ( +
+
+ x +
+
+
{integration?.name}
+
+ CEO @ Gitroom +
+
+ {p.text} +
+ + {!!p?.images?.length && ( +
+ {p.images.map((image, index) => ( + + + + ))} +
+ )} +
+
+ ))}
); }; -const LinkedinSettings: FC = () => { - return
asdfasd
; -}; - -export default withProvider(LinkedinSettings, LinkedinPreview); +export default withProvider(null, LinkedinPreview); diff --git a/apps/frontend/src/components/launches/providers/medium/fonts/Charter Bold Italic.ttf b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Bold Italic.ttf new file mode 100755 index 00000000..8bf24218 Binary files /dev/null and b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Bold Italic.ttf differ diff --git a/apps/frontend/src/components/launches/providers/medium/fonts/Charter Bold.ttf b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Bold.ttf new file mode 100755 index 00000000..c8f7e857 Binary files /dev/null and b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Bold.ttf differ diff --git a/apps/frontend/src/components/launches/providers/medium/fonts/Charter Italic.ttf b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Italic.ttf new file mode 100755 index 00000000..a6501214 Binary files /dev/null and b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Italic.ttf differ diff --git a/apps/frontend/src/components/launches/providers/medium/fonts/Charter Regular.ttf b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Regular.ttf new file mode 100755 index 00000000..33c6d7cb Binary files /dev/null and b/apps/frontend/src/components/launches/providers/medium/fonts/Charter Regular.ttf differ diff --git a/apps/frontend/src/components/launches/providers/medium/fonts/stylesheet.css b/apps/frontend/src/components/launches/providers/medium/fonts/stylesheet.css new file mode 100755 index 00000000..3b63e0fa --- /dev/null +++ b/apps/frontend/src/components/launches/providers/medium/fonts/stylesheet.css @@ -0,0 +1,52 @@ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on July 10, 2013 */ + + + +@font-face { + font-family: 'charterbold_italic'; + src: url('charter_bold_italic-webfont.eot'); + src: url('charter_bold_italic-webfont.eot?#iefix') format('embedded-opentype'), + url('charter_bold_italic-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; + +} + + + + +@font-face { + font-family: 'charterbold'; + src: url('charter_bold-webfont.eot'); + src: url('charter_bold-webfont.eot?#iefix') format('embedded-opentype'), + url('charter_bold-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; + +} + + + + +@font-face { + font-family: 'charteritalic'; + src: url('charter_italic-webfont.eot'); + src: url('charter_italic-webfont.eot?#iefix') format('embedded-opentype'), + url('charter_italic-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; + +} + + + + +@font-face { + font-family: 'charterregular'; + src: url('charter_regular-webfont.eot'); + src: url('charter_regular-webfont.eot?#iefix') format('embedded-opentype'), + url('charter_regular-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; + +} \ No newline at end of file diff --git a/apps/frontend/src/components/launches/providers/medium/medium.provider.tsx b/apps/frontend/src/components/launches/providers/medium/medium.provider.tsx new file mode 100644 index 00000000..e2995d2f --- /dev/null +++ b/apps/frontend/src/components/launches/providers/medium/medium.provider.tsx @@ -0,0 +1,85 @@ +import { FC } from 'react'; +import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { Input } from '@gitroom/react/form/input'; +import { MediumPublications } from '@gitroom/frontend/components/launches/providers/medium/medium.publications'; +import { MediumTags } from '@gitroom/frontend/components/launches/providers/medium/medium.tags'; +import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto'; +import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import clsx from 'clsx'; +import MDEditor from '@uiw/react-md-editor'; +import localFont from 'next/font/local' + +const charter = localFont({ + src: [ + { + path: './fonts/Charter Regular.ttf', + weight: 'normal', + style: 'normal', + }, + { + path: './fonts/Charter Italic.ttf', + weight: 'normal', + style: 'italic', + }, + { + path: './fonts/Charter Bold.ttf', + weight: '700', + style: 'normal', + }, + { + path: './fonts/Charter Bold Italic.ttf', + weight: '700', + style: 'italic', + }, + ], +}); + +const MediumPreview: FC = () => { + const { value } = useIntegration(); + const settings = useSettings(); + const [title, subtitle] = settings.watch([ + 'title', + 'subtitle', + ]); + + return ( +
+
+
{title}
+
{subtitle}
+
+
+ p.content).join('\n')} + /> +
+
+ ); +}; + +const MediumSettings: FC = () => { + const form = useSettings(); + return ( + <> + + + +
+ +
+
+ +
+ + ); +}; + +export default withProvider(MediumSettings, MediumPreview, MediumSettingsDto); diff --git a/apps/frontend/src/components/launches/providers/medium/medium.publications.tsx b/apps/frontend/src/components/launches/providers/medium/medium.publications.tsx new file mode 100644 index 00000000..b95f18ef --- /dev/null +++ b/apps/frontend/src/components/launches/providers/medium/medium.publications.tsx @@ -0,0 +1,44 @@ +import { FC, useEffect, useState } from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { Select } from '@gitroom/react/form/select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; + +export const MediumPublications: FC<{ + name: string; + onChange: (event: { target: { value: string; name: string } }) => void; +}> = (props) => { + const { onChange, name } = props; + const customFunc = useCustomProviderFunction(); + const [publications, setOrgs] = useState([]); + const { getValues } = useSettings(); + const [currentMedia, setCurrentMedia] = useState(); + + const onChangeInner = (event: { target: { value: string, name: string } }) => { + setCurrentMedia(event.target.value); + onChange(event); + }; + + useEffect(() => { + customFunc.get('publications').then((data) => setOrgs(data)); + const settings = getValues()[props.name]; + if (settings) { + setCurrentMedia(settings); + } + }, []); + + + if (!publications.length) { + return null; + } + + return ( + + ); +}; diff --git a/apps/frontend/src/components/launches/providers/medium/medium.tags.tsx b/apps/frontend/src/components/launches/providers/medium/medium.tags.tsx new file mode 100644 index 00000000..1b3ffedb --- /dev/null +++ b/apps/frontend/src/components/launches/providers/medium/medium.tags.tsx @@ -0,0 +1,59 @@ +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { ReactTags } from 'react-tag-autocomplete'; + +export const MediumTags: 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([]); + const [suggestions, setSuggestions] = useState(''); + + 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 ( +
+
{label}
+ +
+ ); +}; diff --git a/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx b/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx index 08500d52..843b1930 100644 --- a/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx +++ b/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx @@ -1,76 +1,187 @@ -import { FC } from 'react'; +import { FC, useCallback } from 'react'; import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; -import clsx from 'clsx'; import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting'; +import { Subreddit } from '@gitroom/frontend/components/launches/providers/reddit/subreddit'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { useFieldArray, useWatch } from 'react-hook-form'; +import { Button } from '@gitroom/react/form/button'; +import { + RedditSettingsDto, + RedditSettingsValueDto, +} from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto'; +import clsx from 'clsx'; +import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; +import {deleteDialog} from "@gitroom/react/helpers/delete.dialog"; -const RedditPreview: FC = (props) => { - const { value: topValue, integration } = useIntegration(); - const newValues = useFormatting(topValue, { +const RenderRedditComponent: FC<{ + type: string; + images?: Array<{ id: string; path: string }>; +}> = (props) => { + const { value: topValue } = useIntegration(); + const showMedia = useMediaDirectory(); + + const { type, images } = props; + + const [firstPost] = useFormatting(topValue, { removeMarkdown: true, saveBreaklines: true, specialFunc: (text: string) => { - return text.slice(0, 280); - } + return text.slice(0, 280); + }, }); + switch (type) { + case 'self': + return ( +
+          {firstPost?.text}
+        
+ ); + case 'link': + return ( +
+ Link +
+ ); + case 'media': + return ( +
+ {!!images?.length && + images.map((image, index) => ( + + + + ))} +
+ ); + } + + return <>; +}; + +const RedditPreview: FC = (props) => { + const { value: topValue, integration } = useIntegration(); + const settings = useWatch({ + name: 'subreddit', + }) as Array; + + const [, ...restOfPosts] = useFormatting(topValue, { + removeMarkdown: true, + saveBreaklines: true, + specialFunc: (text: string) => { + return text.slice(0, 280); + }, + }); + console.log(settings); + + if (!settings || !settings.length) { + return <>Please add at least one Subreddit from the settings; + } + return ( -
-
- {newValues.map((value, index) => ( +
+ {settings + .filter(({ value }) => value?.subreddit) + .map(({ value }, index) => (
-
- x - {index !== topValue.length - 1 && ( -
- )} -
-
-
-
- {integration?.name} -
-
- - - - - -
-
- @username +
+
+
+
+
+ {value.subreddit} +
+
{integration?.name}
-
{value.text}
+
+ {value.title} +
+ +
+ {restOfPosts.map((p, index) => ( +
+
+ x +
+
+
+ {integration?.name} +
+
+                        {p.text}
+                      
+
+
+ ))} +
))} -
); }; const RedditSettings: FC = () => { - return
asdfasd
; + const { register, control } = useSettings(); + const { fields, append, remove } = useFieldArray({ + control, // control props comes from useForm (optional: if you are using FormContext) + name: 'subreddit', // unique name for your Field Array + }); + + const addField = useCallback(() => { + append({}); + }, [fields, append]); + + const deleteField = useCallback((index: number) => async () => { + if (!await deleteDialog('Are you sure you want to delete this Subreddit?')) return; + remove(index); + }, [fields, remove]); + + return ( + <> +
+ {fields.map((field, index) => ( +
+
+ x +
+ +
+ ))} +
+ + {fields.length === 0 && ( +
+ Please add at least one Subreddit +
+ )} + + ); }; -export default withProvider(RedditSettings, RedditPreview); +export default withProvider(RedditSettings, RedditPreview, RedditSettingsDto); diff --git a/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx b/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx new file mode 100644 index 00000000..fb753d02 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx @@ -0,0 +1,286 @@ +import { + FC, + FormEvent, + useCallback, + useMemo, + useState, +} from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { Input } from '@gitroom/react/form/input'; +import { useDebouncedCallback } from 'use-debounce'; +import { Button } from '@gitroom/react/form/button'; +import clsx from 'clsx'; +import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; +import { useWatch } from 'react-hook-form'; +import { Select } from '@gitroom/react/form/select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; + +export const RenderOptions: FC<{ + options: Array<'self' | 'link' | 'media'>; + onClick: (current: 'self' | 'link' | 'media') => void; + value: 'self' | 'link' | 'media'; +}> = (props) => { + const { options, onClick, value } = props; + const mapValues = useMemo(() => { + return options.map((p) => ({ + children: ( + <> + {p === 'self' + ? 'Post' + : p === 'link' + ? 'Link' + : p === 'media' + ? 'Media' + : ''} + + ), + id: p, + onClick: () => onClick(p), + })); + }, [options]); + + return ( +
+ {mapValues.map((p) => ( +
+ ); +}; +export const Subreddit: FC<{ + onChange: (event: { + target: { name: string; value: { id: string; name: string } }; + }) => void; + name: string; +}> = (props) => { + const { onChange, name } = props; + + const state = useSettings(); + const split = name.split('.'); + const [loading, setLoading] = useState(false); + // @ts-ignore + const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value; + + const [results, setResults] = useState([]); + const func = useCustomProviderFunction(); + const value = useWatch({ name }); + const [searchValue, setSearchValue] = useState(''); + + const setResult = (result: { id: string; name: string }) => async () => { + setLoading(true); + setSearchValue(''); + const restrictions = await func.get('restrictions', { + subreddit: result.name, + }); + + onChange({ + target: { + name, + value: { + ...restrictions, + type: restrictions.allow[0], + media: [], + }, + }, + }); + + setLoading(false); + }; + + const setTitle = useCallback( + (e: any) => { + onChange({ + target: { + name, + value: { + ...value, + title: e.target.value, + }, + }, + }); + }, + [value] + ); + + const setType = useCallback( + (e: string) => { + onChange({ + target: { + name, + value: { + ...value, + type: e, + }, + }, + }); + }, + [value] + ); + + const setMedia = useCallback( + (e: any) => { + onChange({ + target: { + name, + value: { + ...value, + media: e.target.value.map((p: any) => p), + }, + }, + }); + }, + [value] + ); + + const setURL = useCallback( + (e: any) => { + onChange({ + target: { + name, + value: { + ...value, + url: e.target.value, + }, + }, + }); + }, + [value] + ); + + const setFlair = useCallback( + (e: any) => { + onChange({ + target: { + name, + value: { + ...value, + flair: value.flairs.find((p: any) => p.id === e.target.value), + }, + }, + }); + }, + [value] + ); + + const search = useDebouncedCallback( + useCallback(async (e: FormEvent) => { + // @ts-ignore + setResults([]); + // @ts-ignore + if (!e.target.value) { + return; + } + // @ts-ignore + const results = await func.get('subreddits', { word: e.target.value }); + // @ts-ignore + setResults(results); + }, []), + 500 + ); + + return ( +
+ {value?.subreddit ? ( + <> + +
+ +
+ + + {value.type === 'link' && ( + + )} + {value.type === 'media' && ( +
+
+
+ +
+
+ )} + + ) : ( +
+ { + // @ts-ignore + setSearchValue(e.target.value); + await search(e); + }} + /> + {!!results.length && !loading && ( +
+ {results.map((r: { id: string; name: string }) => ( +
+ {r.name} +
+ ))} +
+ )} +
+ )} +
+ ); +}; diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index b8d5bc5c..174b7959 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -4,14 +4,20 @@ import DevtoProvider from "@gitroom/frontend/components/launches/providers/devto import XProvider from "@gitroom/frontend/components/launches/providers/x/x.provider"; import LinkedinProvider from "@gitroom/frontend/components/launches/providers/linkedin/linkedin.provider"; import RedditProvider from "@gitroom/frontend/components/launches/providers/reddit/reddit.provider"; +import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider"; +import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider"; const Providers = [ {identifier: 'devto', component: DevtoProvider}, {identifier: 'x', component: XProvider}, {identifier: 'linkedin', component: LinkedinProvider}, {identifier: 'reddit', component: RedditProvider}, + {identifier: 'medium', component: MediumProvider}, + {identifier: 'hashnode', component: HashnodeProvider}, ]; + + export const ShowAllProviders: FC<{integrations: Integrations[], value: Array<{content: string, id?: string}>, selectedProvider?: Integrations}> = (props) => { const {integrations, value, selectedProvider} = props; return ( diff --git a/apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Bold.woff2 b/apps/frontend/src/components/launches/providers/x/fonts/Chirp-Bold.woff2 similarity index 100% rename from apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Bold.woff2 rename to apps/frontend/src/components/launches/providers/x/fonts/Chirp-Bold.woff2 diff --git a/apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Regular.woff2 b/apps/frontend/src/components/launches/providers/x/fonts/Chirp-Regular.woff2 similarity index 100% rename from apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Regular.woff2 rename to apps/frontend/src/components/launches/providers/x/fonts/Chirp-Regular.woff2 diff --git a/apps/frontend/src/components/launches/providers/x/x.provider.tsx b/apps/frontend/src/components/launches/providers/x/x.provider.tsx index b61a9003..0481cc16 100644 --- a/apps/frontend/src/components/launches/providers/x/x.provider.tsx +++ b/apps/frontend/src/components/launches/providers/x/x.provider.tsx @@ -4,17 +4,17 @@ import localFont from 'next/font/local'; import clsx from 'clsx'; import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting'; -import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values"; +import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; const chirp = localFont({ src: [ { - path: './fonts/x/Chirp-Regular.woff2', + path: './fonts/Chirp-Regular.woff2', weight: '400', style: 'normal', }, { - path: './fonts/x/Chirp-Bold.woff2', + path: './fonts/Chirp-Bold.woff2', weight: '700', style: 'normal', }, @@ -23,28 +23,26 @@ const chirp = localFont({ const XPreview: FC = (props) => { const { value: topValue, integration } = useIntegration(); + const mediaDir = useMediaDirectory(); const newValues = useFormatting(topValue, { removeMarkdown: true, saveBreaklines: true, specialFunc: (text: string) => { - return text.slice(0, 280); - } + return text.slice(0, 280); + }, }); return ( -
-
+
+
{newValues.map((value, index) => (
{ @username
-
{value.text}
+
+                {value.text}
+              
+ {!!value?.images?.length && ( +
+ {value.images.map((image, index) => ( + + + + ))} +
+ )}
))} @@ -87,11 +99,4 @@ const XPreview: FC = (props) => { ); }; -const XSettings: FC = () => { - const settings = useSettings({ - - }); - return
asdfasd
; -}; - -export default withProvider(XSettings, XPreview); +export default withProvider(null, XPreview); diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 95ce3af7..e7bb4d83 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -1,38 +1,51 @@ -import {ReactNode} from "react"; -import {Title} from "@gitroom/frontend/components/layout/title"; -import {headers} from "next/headers"; -import {ContextWrapper} from "@gitroom/frontend/components/layout/user.context"; -import {NotificationComponent} from "@gitroom/frontend/components/notifications/notification.component"; -import {TopMenu} from "@gitroom/frontend/components/layout/top.menu"; -import {MantineWrapper} from "@gitroom/react/helpers/mantine.wrapper"; -import {ToolTip} from "@gitroom/frontend/components/layout/top.tip"; +import { ReactNode } from 'react'; +import { Title } from '@gitroom/frontend/components/layout/title'; +import { headers } from 'next/headers'; +import { ContextWrapper } from '@gitroom/frontend/components/layout/user.context'; +import { TopMenu } from '@gitroom/frontend/components/layout/top.menu'; +import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper'; +import { ToolTip } from '@gitroom/frontend/components/layout/top.tip'; +import { ShowMediaBoxModal } from '@gitroom/frontend/components/media/media.component'; +import Image from 'next/image'; +import dynamic from 'next/dynamic'; -export const LayoutSettings = ({children}: {children: ReactNode}) => { - const user = JSON.parse(headers().get('user')!); - return ( - - - -
-
-
- Gitroom -
- -
- -
-
-
-
- - <div className="flex flex-1 flex-col"> - {children} - </div> - </div> - </div> - </div> - </MantineWrapper> - </ContextWrapper> - ); -} \ No newline at end of file +const NotificationComponent = dynamic( + () => + import('@gitroom/frontend/components/notifications/notification.component'), + { + loading: () => <></>, + ssr: false, + } +); + +export const LayoutSettings = ({ children }: { children: ReactNode }) => { + const user = JSON.parse(headers().get('user')!); + return ( + <ContextWrapper user={user}> + <MantineWrapper> + <ToolTip /> + <ShowMediaBoxModal /> + <div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col"> + <div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary"> + <div className="text-2xl flex items-center gap-[10px]"> + <div> + <Image src="/logo.svg" width={55} height={53} alt="Logo" /> + </div> + <div className="mt-[12px]">Gitroom</div> + </div> + <TopMenu /> + <div> + <NotificationComponent /> + </div> + </div> + <div className="flex-1 flex"> + <div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col"> + <Title /> + <div className="flex flex-1 flex-col">{children}</div> + </div> + </div> + </div> + </MantineWrapper> + </ContextWrapper> + ); +}; diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index 75934646..41365738 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -40,7 +40,7 @@ export const TopMenu: FC = () => { <ul className="gap-5 flex flex-1 items-center text-[18px]"> {menuItems.map((item, index) => ( <li key={item.name}> - <Link href={item.path} className={clsx("flex gap-2 items-center box", menuItems.map(p => p.path).indexOf(path) === index ? 'text-primary showbox' : 'text-gray')}> + <Link prefetch={false} href={item.path} className={clsx("flex gap-2 items-center box", menuItems.map(p => p.path).indexOf(path) === index ? 'text-primary showbox' : 'text-gray')}> <span>{item.name}</span> </Link> </li> diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index d95b989c..cc188028 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -6,8 +6,45 @@ import useSWR from 'swr'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { Media } from '@prisma/client'; import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; -import {useFormState} from "react-hook-form"; -import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values"; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import EventEmitter from 'events'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import clsx from 'clsx'; +const showModalEmitter = new EventEmitter(); + +export const ShowMediaBoxModal: FC = () => { + const [showModal, setShowModal] = useState(false); + const [callBack, setCallBack] = + useState<(params: { id: string; path: string }) => void | undefined>(); + + const closeModal = useCallback(() => { + setShowModal(false); + setCallBack(undefined); + }, []); + + useEffect(() => { + showModalEmitter.on('show-modal', (cCallback) => { + setShowModal(true); + setCallBack(() => cCallback); + }); + return () => { + showModalEmitter.removeAllListeners('show-modal'); + }; + }, []); + if (!showModal) return null; + + return ( + <div className="text-white"> + <MediaBox setMedia={callBack!} closeModal={closeModal} /> + </div> + ); +}; + +export const showMediaBox = ( + callback: (params: { id: string; path: string }) => void +) => { + showModalEmitter.emit('show-modal', callback); +}; export const MediaBox: FC<{ setMedia: (params: { id: string; path: string }) => void; @@ -67,62 +104,104 @@ export const MediaBox: FC<{ return ( <div className="fixed left-0 top-0 bg-black/80 z-[300] w-full h-full p-[60px] animate-fade"> - <div className="w-full h-full bg-black border-tableBorder border-2 rounded-xl p-[20px] relative"> - <button - onClick={closeModal} - className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black 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> - - <button - className="flex absolute right-[40px] top-[7px] pointer hover:bg-third rounded-lg transition-all group px-2.5 py-2.5 text-sm font-semibold bg-transparent text-gray-800 hover:bg-gray-100 focus:text-primary-500" - type="button" - > - <div className="relative flex gap-2 items-center justify-center"> - <input - type="file" - className="absolute left-0 top-0 w-full h-full opacity-0" - accept="image/*" - onChange={uploadMedia} - /> - <span className="sc-dhKdcB fhJPPc w-4 h-4"> - <svg - width="18" - height="18" - viewBox="0 0 18 18" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.5276 1.00176C7.3957 0.979897 8.25623 1.16248 9.04309 1.53435C9.82982 1.90617 10.5209 2.45677 11.065 3.14199C11.3604 3.51404 11.6084 3.92054 11.8045 4.3516C12.2831 4.21796 12.7853 4.17281 13.2872 4.22273C14.2108 4.3146 15.0731 4.72233 15.7374 5.3744C16.4012 6.02599 16.8292 6.88362 16.9586 7.808C17.088 8.73224 16.9124 9.67586 16.457 10.4887C16.1871 10.9706 15.5777 11.1424 15.0958 10.8724C14.614 10.6025 14.4422 9.99308 14.7122 9.51126C14.9525 9.08224 15.0471 8.57971 14.9779 8.08532C14.9087 7.59107 14.6807 7.13971 14.3364 6.8017C13.9925 6.46418 13.5528 6.25903 13.0892 6.21291C12.6258 6.16682 12.1584 6.28157 11.7613 6.5429C11.4874 6.7232 11.1424 6.7577 10.8382 6.63524C10.534 6.51278 10.3091 6.24893 10.2365 5.92912C10.1075 5.36148 9.8545 4.83374 9.49872 4.38568C9.14303 3.93773 8.69439 3.58166 8.18851 3.34258C7.68275 3.10355 7.13199 2.98717 6.57794 3.00112C6.02388 3.01507 5.47902 3.15905 4.98477 3.4235C4.49039 3.68801 4.05875 4.06664 3.72443 4.53247C3.39004 4.9984 3.16233 5.5387 3.06049 6.11239C2.95864 6.68613 2.98571 7.27626 3.1394 7.83712C3.29306 8.39792 3.56876 8.91296 3.94345 9.34361C4.30596 9.76027 4.26207 10.3919 3.84542 10.7544C3.42876 11.1169 2.79712 11.073 2.4346 10.6564C1.8607 9.99678 1.44268 9.213 1.2105 8.36566C0.978333 7.51837 0.937639 6.62828 1.09128 5.76282C1.24492 4.89732 1.58919 4.07751 2.09958 3.36634C2.61005 2.65507 3.27363 2.07075 4.04125 1.66005C4.80899 1.24927 5.65951 1.02361 6.5276 1.00176Z" - fill="currentColor" - ></path> - <path - d="M8 12.4142L8 17C8 17.5523 8.44771 18 9 18C9.55228 18 10 17.5523 10 17V12.4142L11.2929 13.7071C11.6834 14.0976 12.3166 14.0976 12.7071 13.7071C13.0976 13.3166 13.0976 12.6834 12.7071 12.2929L9.70711 9.29289C9.61123 9.19702 9.50073 9.12468 9.38278 9.07588C9.26488 9.02699 9.13559 9 9 9C8.86441 9 8.73512 9.02699 8.61722 9.07588C8.50195 9.12357 8.3938 9.19374 8.29945 9.2864C8.29705 9.28875 8.29467 9.29111 8.2923 9.29349L5.29289 12.2929C4.90237 12.6834 4.90237 13.3166 5.29289 13.7071C5.68342 14.0976 6.31658 14.0976 6.70711 13.7071L8 12.4142Z" - fill="currentColor" - ></path> - </svg> - </span> - <span>Upload assets</span> + <div className="w-full h-full bg-[#0B101B] border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative"> + <div className="flex"> + <div className="flex-1"> + <TopTitle title="Media Library" /> </div> - </button> + <button + onClick={closeModal} + className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black 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 className="flex flex-wrap gap-[10px] mt-[35px] pt-[20px] border-tableBorder border-t-2"> + {!!mediaList.length && ( + <button + className="flex absolute right-[40px] top-[7px] pointer hover:bg-third rounded-lg transition-all group px-2.5 py-2.5 text-sm font-semibold bg-transparent text-gray-800 hover:bg-gray-100 focus:text-primary-500" + type="button" + > + <div className="relative flex gap-2 items-center justify-center"> + <input + type="file" + className="absolute left-0 top-0 w-full h-full opacity-0" + accept="image/*" + onChange={uploadMedia} + /> + <button className="cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white font-['Inter'] border-[2px] border-[#506490]"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + width="14" + height="15" + viewBox="0 0 14 15" + fill="none" + > + <path + d="M7 1.8125C5.87512 1.8125 4.7755 2.14607 3.8402 2.77102C2.90489 3.39597 2.17591 4.28423 1.74544 5.32349C1.31496 6.36274 1.20233 7.50631 1.42179 8.60958C1.64124 9.71284 2.18292 10.7263 2.97833 11.5217C3.77374 12.3171 4.78716 12.8588 5.89043 13.0782C6.99369 13.2977 8.13726 13.185 9.17651 12.7546C10.2158 12.3241 11.104 11.5951 11.729 10.6598C12.3539 9.7245 12.6875 8.62488 12.6875 7.5C12.6859 5.99207 12.0862 4.54636 11.0199 3.48009C9.95365 2.41382 8.50793 1.81409 7 1.8125ZM7 12.3125C6.04818 12.3125 5.11773 12.0303 4.32632 11.5014C3.53491 10.9726 2.91808 10.221 2.55383 9.34166C2.18959 8.46229 2.09428 7.49466 2.27997 6.56113C2.46566 5.62759 2.92401 4.77009 3.59705 4.09705C4.27009 3.42401 5.1276 2.96566 6.06113 2.77997C6.99466 2.59428 7.9623 2.68958 8.84167 3.05383C9.72104 3.41808 10.4726 4.03491 11.0015 4.82632C11.5303 5.61773 11.8125 6.54818 11.8125 7.5C11.8111 8.77591 11.3036 9.99915 10.4014 10.9014C9.49915 11.8036 8.27591 12.3111 7 12.3125ZM9.625 7.5C9.625 7.61603 9.57891 7.72731 9.49686 7.80936C9.41481 7.89141 9.30353 7.9375 9.1875 7.9375H7.4375V9.6875C7.4375 9.80353 7.39141 9.91481 7.30936 9.99686C7.22731 10.0789 7.11603 10.125 7 10.125C6.88397 10.125 6.77269 10.0789 6.69064 9.99686C6.6086 9.91481 6.5625 9.80353 6.5625 9.6875V7.9375H4.8125C4.69647 7.9375 4.58519 7.89141 4.50314 7.80936C4.4211 7.72731 4.375 7.61603 4.375 7.5C4.375 7.38397 4.4211 7.27269 4.50314 7.19064C4.58519 7.10859 4.69647 7.0625 4.8125 7.0625H6.5625V5.3125C6.5625 5.19647 6.6086 5.08519 6.69064 5.00314C6.77269 4.92109 6.88397 4.875 7 4.875C7.11603 4.875 7.22731 4.92109 7.30936 5.00314C7.39141 5.08519 7.4375 5.19647 7.4375 5.3125V7.0625H9.1875C9.30353 7.0625 9.41481 7.10859 9.49686 7.19064C9.57891 7.27269 9.625 7.38397 9.625 7.5Z" + fill="white" + /> + </svg> + </div> + <div>Upload</div> + </button> + </div> + </button> + )} + </div> + <div + className={clsx( + 'flex flex-wrap gap-[10px] mt-[35px] pt-[20px]', + !mediaList.length && 'justify-center items-center text-white' + )} + > + {!mediaList.length && ( + <div className="flex flex-col text-center"> + <div>You don{"'"}t have any assets yet.</div> + <div>Click the button below to upload one</div> + <div className="mt-[10px]"> + <div className="relative flex gap-2 items-center justify-center"> + <input + type="file" + className="absolute left-0 top-0 w-full h-full opacity-0" + accept="image/*" + onChange={uploadMedia} + /> + <button className="cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white font-['Inter'] border-[2px] border-[#506490]"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + width="14" + height="15" + viewBox="0 0 14 15" + fill="none" + > + <path + d="M7 1.8125C5.87512 1.8125 4.7755 2.14607 3.8402 2.77102C2.90489 3.39597 2.17591 4.28423 1.74544 5.32349C1.31496 6.36274 1.20233 7.50631 1.42179 8.60958C1.64124 9.71284 2.18292 10.7263 2.97833 11.5217C3.77374 12.3171 4.78716 12.8588 5.89043 13.0782C6.99369 13.2977 8.13726 13.185 9.17651 12.7546C10.2158 12.3241 11.104 11.5951 11.729 10.6598C12.3539 9.7245 12.6875 8.62488 12.6875 7.5C12.6859 5.99207 12.0862 4.54636 11.0199 3.48009C9.95365 2.41382 8.50793 1.81409 7 1.8125ZM7 12.3125C6.04818 12.3125 5.11773 12.0303 4.32632 11.5014C3.53491 10.9726 2.91808 10.221 2.55383 9.34166C2.18959 8.46229 2.09428 7.49466 2.27997 6.56113C2.46566 5.62759 2.92401 4.77009 3.59705 4.09705C4.27009 3.42401 5.1276 2.96566 6.06113 2.77997C6.99466 2.59428 7.9623 2.68958 8.84167 3.05383C9.72104 3.41808 10.4726 4.03491 11.0015 4.82632C11.5303 5.61773 11.8125 6.54818 11.8125 7.5C11.8111 8.77591 11.3036 9.99915 10.4014 10.9014C9.49915 11.8036 8.27591 12.3111 7 12.3125ZM9.625 7.5C9.625 7.61603 9.57891 7.72731 9.49686 7.80936C9.41481 7.89141 9.30353 7.9375 9.1875 7.9375H7.4375V9.6875C7.4375 9.80353 7.39141 9.91481 7.30936 9.99686C7.22731 10.0789 7.11603 10.125 7 10.125C6.88397 10.125 6.77269 10.0789 6.69064 9.99686C6.6086 9.91481 6.5625 9.80353 6.5625 9.6875V7.9375H4.8125C4.69647 7.9375 4.58519 7.89141 4.50314 7.80936C4.4211 7.72731 4.375 7.61603 4.375 7.5C4.375 7.38397 4.4211 7.27269 4.50314 7.19064C4.58519 7.10859 4.69647 7.0625 4.8125 7.0625H6.5625V5.3125C6.5625 5.19647 6.6086 5.08519 6.69064 5.00314C6.77269 4.92109 6.88397 4.875 7 4.875C7.11603 4.875 7.22731 4.92109 7.30936 5.00314C7.39141 5.08519 7.4375 5.19647 7.4375 5.3125V7.0625H9.1875C9.30353 7.0625 9.41481 7.10859 9.49686 7.19064C9.57891 7.27269 9.625 7.38397 9.625 7.5Z" + fill="white" + /> + </svg> + </div> + <div>Upload</div> + </button> + </div> + </div> + </div> + )} {mediaList.map((media) => ( <div key={media.id} @@ -140,6 +219,103 @@ export const MediaBox: FC<{ </div> ); }; + +export const MultiMediaComponent: FC<{ + label: string; + description: string; + value?: Array<{ path: string; id: string }>; + name: string; + error?: any; + onChange: (event: { + target: { name: string; value?: Array<{ id: string; path: string }> }; + }) => void; +}> = (props) => { + const { name, label, error, description, onChange, value } = props; + useEffect(() => { + if (value) { + setCurrentMedia(value); + } + }, []); + + const [modal, setShowModal] = useState(false); + const [currentMedia, setCurrentMedia] = useState(value); + const mediaDirectory = useMediaDirectory(); + + const changeMedia = useCallback( + (m: { path: string; id: string }) => { + const newMedia = [...(currentMedia || []), m]; + setCurrentMedia(newMedia); + onChange({ target: { name, value: newMedia } }); + }, + [currentMedia] + ); + + const showModal = useCallback(() => { + setShowModal(!modal); + }, [modal]); + + const clearMedia = useCallback( + (topIndex: number) => () => { + const newMedia = currentMedia?.filter((f, index) => index !== topIndex); + setCurrentMedia(newMedia); + onChange({ target: { name, value: newMedia } }); + }, + [currentMedia] + ); + + return ( + <> + <div className="flex flex-col gap-[8px] bg-[#131B2C] rounded-bl-[8px]"> + {modal && <MediaBox setMedia={changeMedia} closeModal={showModal} />} + <div className="flex gap-[10px]"> + <div className="flex"> + <Button + onClick={showModal} + className="ml-[10px] rounded-[4px] mb-[10px] gap-[8px] justify-center items-center w-[127px] flex border border-dashed border-[#506490] bg-[#131B2C]" + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + > + <path + d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z" + fill="white" + /> + </svg> + </div> + <div className="text-[12px] font-[500]">Insert Media</div> + </Button> + </div> + + {!!currentMedia && + currentMedia.map((media, index) => ( + <> + <div className="cursor-pointer w-[40px] h-[40px] border-2 border-tableBorder relative"> + <img + className="w-full h-full object-cover" + src={mediaDirectory.set(media.path)} + onClick={() => window.open(mediaDirectory.set(media.path))} + /> + <div + onClick={clearMedia(index)} + className="rounded-full w-[15px] h-[15px] bg-red-800 text-white flex justify-center items-center absolute -right-[4px] -top-[4px]" + > + x + </div> + </div> + </> + ))} + </div> + </div> + <div className="text-[12px] text-red-400">{error}</div> + </> + ); +}; + export const MediaComponent: FC<{ label: string; description: string; @@ -150,7 +326,7 @@ export const MediaComponent: FC<{ }) => void; }> = (props) => { const { name, label, description, onChange, value } = props; - const {getValues} = useSettings(); + const { getValues } = useSettings(); useEffect(() => { const settings = getValues()[props.name]; if (settings) { diff --git a/apps/frontend/src/components/notifications/notification.component.tsx b/apps/frontend/src/components/notifications/notification.component.tsx index 4de8d430..0c6dbe00 100644 --- a/apps/frontend/src/components/notifications/notification.component.tsx +++ b/apps/frontend/src/components/notifications/notification.component.tsx @@ -3,7 +3,7 @@ import {NotificationBell, NovuProvider, PopoverNotificationCenter} from "@novu/notification-center"; import {useUser} from "@gitroom/frontend/components/layout/user.context"; -export const NotificationComponent = () => { +const NotificationComponent = () => { const user = useUser(); return ( <NovuProvider @@ -15,4 +15,6 @@ export const NotificationComponent = () => { </PopoverNotificationCenter> </NovuProvider> ) -} \ No newline at end of file +} + +export default NotificationComponent; \ No newline at end of file diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index b836639e..8c3cb6eb 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -32,16 +32,34 @@ module.exports = { gridTemplateColumns: { '13': 'repeat(13, minmax(0, 1fr));' }, + backgroundImage: { + loginBox: 'url(/auth/login-box.png)', + loginBg: 'url(/auth/bg-login.png)' + }, animation: { fade: 'fadeOut 0.5s ease-in-out', + overflow: 'overFlow 0.5s ease-in-out forwards', + overflowReverse: 'overFlowReverse 0.5s ease-in-out forwards', + }, + boxShadow: { + yellow: '0 0 60px 20px #6b6237' }, - // that is actual animation keyframes: theme => ({ fadeOut: { '0%': { opacity: 0, transform: 'translateY(30px)' }, '100%': { opacity: 1, transform: 'translateY(0)' }, }, + overFlow: { + '0%': { overflow: 'hidden' }, + '99%': { overflow: 'hidden' }, + '100%': { overflow: 'visible' }, + }, + overFlowReverse: { + '0%': { overflow: 'visible' }, + '99%': { overflow: 'visible' }, + '100%': { overflow: 'hidden' }, + }, }) }, }, diff --git a/libraries/helpers/src/utils/custom.fetch.func.ts b/libraries/helpers/src/utils/custom.fetch.func.ts index a6e84fe3..b118c43a 100644 --- a/libraries/helpers/src/utils/custom.fetch.func.ts +++ b/libraries/helpers/src/utils/custom.fetch.func.ts @@ -21,6 +21,7 @@ export const customFetch = (params: Params, auth?: string) => { Accept: 'application/json', ...options?.headers, }, + cache: options.cache || 'no-store', }); await params?.afterRequest?.(url, options, fetchRequest); return fetchRequest; diff --git a/libraries/nestjs-libraries/src/database/prisma/comments/comments.repository.ts b/libraries/nestjs-libraries/src/database/prisma/comments/comments.repository.ts new file mode 100644 index 00000000..232ed55a --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/comments/comments.repository.ts @@ -0,0 +1,163 @@ +import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; +import { Injectable } from '@nestjs/common'; +import dayjs from 'dayjs'; +import { groupBy } from 'lodash'; + +@Injectable() +export class CommentsRepository { + constructor(private _media: PrismaRepository<'comments'>) {} + + addAComment(orgId: string, userId: string, comment: string, date: string) { + return this._media.model.comments.create({ + data: { + organizationId: orgId, + userId: userId, + content: comment, + date: dayjs(date).toDate(), + }, + select: { + id: true, + }, + }); + } + + addACommentToComment( + orgId: string, + userId: string, + commentId: string, + comment: string, + date: string + ) { + return this._media.model.comments.create({ + data: { + organizationId: orgId, + userId: userId, + content: comment, + date: dayjs(date).toDate(), + parentCommentId: commentId, + }, + select: { + id: true, + }, + }); + } + + updateAComment( + orgId: string, + userId: string, + commentId: string, + comment: string + ) { + return this._media.model.comments.update({ + where: { + id: commentId, + organizationId: orgId, + userId: userId, + }, + data: { + content: comment, + }, + }); + } + + deleteAComment(orgId: string, userId: string, commentId: string) { + return this._media.model.comments.update({ + where: { + id: commentId, + organizationId: orgId, + userId: userId, + }, + data: { + deletedAt: new Date(), + }, + }); + } + + async loadAllCommentsAndSubCommentsForADate(orgId: string, date: string) { + return this._media.model.comments.findMany({ + where: { + organizationId: orgId, + deletedAt: null, + date: dayjs(date).toDate(), + parentCommentId: null, + }, + orderBy: { + createdAt: 'desc', + }, + select: { + id: true, + content: true, + user: { + select: { + id: true, + email: true, + }, + }, + childrenComment: { + where: { + deletedAt: null, + }, + select: { + id: true, + content: true, + user: { + select: { + id: true, + email: true, + }, + }, + }, + }, + }, + }); + } + + async getAllCommentsByWeekYear(orgId: string, year: number, week: number) { + const date = dayjs().year(year).isoWeek(week); + const startDate = date.startOf('isoWeek').toDate(); + const endDate = date.endOf('isoWeek').toDate(); + + const load = await this._media.model.comments.findMany({ + where: { + organizationId: orgId, + deletedAt: null, + parentCommentId: null, + date: { + gte: startDate, + lte: endDate, + }, + }, + select: { + date: true, + _count: { + select: { + childrenComment: { + where: { + deletedAt: null, + } + } + }, + }, + }, + }); + + const group = groupBy(load, (item) => + dayjs(item.date).format('YYYY-MM-DD HH:MM') + ); + + return Object.values(group).reduce((all, current) => { + return [ + ...all, + { + date: current[0].date, + total: + current.length + + current.reduce( + (all2, current2) => all2 + current2._count.childrenComment, + 0 + ), + }, + ]; + }, [] as Array<{ date: Date; total: number }>); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/comments/comments.service.ts b/libraries/nestjs-libraries/src/database/prisma/comments/comments.service.ts new file mode 100644 index 00000000..7882a01e --- /dev/null +++ b/libraries/nestjs-libraries/src/database/prisma/comments/comments.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository'; + +@Injectable() +export class CommentsService { + constructor(private _commentsRepository: CommentsRepository) {} + + addAComment(orgId: string, userId: string, comment: string, date: string) { + return this._commentsRepository.addAComment(orgId, userId, comment, date); + } + + addACommentToComment( + orgId: string, + userId: string, + commentId: string, + comment: string, + date: string + ) { + return this._commentsRepository.addACommentToComment(orgId, userId, commentId, comment, date); + } + + updateAComment( + orgId: string, + userId: string, + commentId: string, + comment: string + ) { + return this._commentsRepository.updateAComment( + orgId, + userId, + commentId, + comment + ); + } + + deleteAComment(orgId: string, userId: string, commentId: string) { + return this._commentsRepository.deleteAComment(orgId, userId, commentId); + } + + loadAllCommentsAndSubCommentsForADate(orgId: string, date: string) { + return this._commentsRepository.loadAllCommentsAndSubCommentsForADate( + orgId, + date + ); + } + + getAllCommentsByWeekYear(orgId: string, year: number, week: number) { + return this._commentsRepository.getAllCommentsByWeekYear(orgId, year, week); + } +} diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts index eb0025e6..dfe6f039 100644 --- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts +++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts @@ -1,50 +1,52 @@ -import {Global, Module} from "@nestjs/common"; -import {PrismaRepository, PrismaService} from "./prisma.service"; -import {OrganizationRepository} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository"; -import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; -import {UsersService} from "@gitroom/nestjs-libraries/database/prisma/users/users.service"; -import {UsersRepository} from "@gitroom/nestjs-libraries/database/prisma/users/users.repository"; -import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service"; -import {StarsRepository} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.repository"; -import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service"; -import {SubscriptionRepository} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository"; -import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service"; -import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service"; -import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository"; -import {PostsService} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.service"; -import {PostsRepository} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.repository"; -import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integration.manager"; -import {MediaService} from "@gitroom/nestjs-libraries/database/prisma/media/media.service"; -import {MediaRepository} from "@gitroom/nestjs-libraries/database/prisma/media/media.repository"; +import { Global, Module } from '@nestjs/common'; +import { PrismaRepository, PrismaService } from './prisma.service'; +import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; +import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; +import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository'; +import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service'; +import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository'; +import { NotificationService } from '@gitroom/nestjs-libraries/notifications/notification.service'; +import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; +import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository'; +import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; +import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository'; +import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; +import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service'; +import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository'; +import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository'; +import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service'; @Global() @Module({ - imports: [], - controllers: [], - providers: [ - PrismaService, - PrismaRepository, - UsersService, - UsersRepository, - OrganizationService, - OrganizationRepository, - StarsService, - StarsRepository, - SubscriptionService, - SubscriptionRepository, - NotificationService, - IntegrationService, - IntegrationRepository, - PostsService, - PostsRepository, - MediaService, - MediaRepository, - IntegrationManager - ], - get exports() { - return this.providers; - } + imports: [], + controllers: [], + providers: [ + PrismaService, + PrismaRepository, + UsersService, + UsersRepository, + OrganizationService, + OrganizationRepository, + StarsService, + StarsRepository, + SubscriptionService, + SubscriptionRepository, + NotificationService, + IntegrationService, + IntegrationRepository, + PostsService, + PostsRepository, + MediaService, + MediaRepository, + CommentsRepository, + CommentsService, + IntegrationManager, + ], + get exports() { + return this.providers; + }, }) -export class DatabaseModule { - -} \ No newline at end of file +export class DatabaseModule {} 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 ecc9cca0..bd787658 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -1,13 +1,11 @@ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; import { Injectable } from '@nestjs/common'; import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto'; -import { Integration, Post } from '@prisma/client'; +import { Post } from '@prisma/client'; import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto'; import dayjs from 'dayjs'; import isoWeek from 'dayjs/plugin/isoWeek'; import { v4 as uuidv4 } from 'uuid'; -import {instanceToInstance, instanceToPlain} from "class-transformer"; -import {validate} from "class-validator"; dayjs.extend(isoWeek); @@ -28,6 +26,7 @@ export class PostsRepository { gte: startDate, lte: endDate, }, + deletedAt: null, parentPostId: null, }, select: { @@ -46,11 +45,35 @@ export class PostsRepository { }); } + async deletePost(orgId: string, group: string) { + await this._post.model.post.updateMany({ + where: { + organizationId: orgId, + group, + }, + data: { + deletedAt: new Date(), + }, + }); + + return this._post.model.post.findFirst({ + where: { + organizationId: orgId, + group, + parentPostId: null, + }, + select: { + id: true, + }, + }); + } + getPost(id: string, includeIntegration = false, orgId?: string) { return this._post.model.post.findUnique({ where: { id, ...(orgId ? { organizationId: orgId } : {}), + deletedAt: null, }, include: { ...(includeIntegration ? { integration: true } : {}), @@ -84,11 +107,12 @@ export class PostsRepository { }); } - async createOrUpdatePost(orgId: string, date: string, body: PostBody) { + async createOrUpdatePost(state: 'draft' | 'schedule', orgId: string, date: string, body: PostBody) { const posts: Post[] = []; + const uuid = uuidv4(); for (const value of body.value) { - const updateData = { + const updateData = (type: 'create' | 'update') => ({ publishDate: dayjs(date).toDate(), integration: { connect: { @@ -96,7 +120,7 @@ export class PostsRepository { organizationId: orgId, }, }, - ...(posts.length + ...(posts?.[posts.length - 1]?.id ? { parentPost: { connect: { @@ -104,28 +128,64 @@ export class PostsRepository { }, }, } + : type === 'update' + ? { + parentPost: { + disconnect: true, + }, + } : {}), content: value.content, + group: uuid, + state: state === 'draft' ? 'DRAFT' as const : 'QUEUE' as const, + image: JSON.stringify(value.image), settings: JSON.stringify(body.settings), organization: { connect: { id: orgId, }, }, - }; - + }); posts.push( await this._post.model.post.upsert({ where: { - id: value.id || uuidv4() + id: value.id || uuidv4(), }, - create: updateData, - update: updateData, + create: updateData('create'), + update: updateData('update'), }) ); } - return posts; + const previousPost = body.group + ? ( + await this._post.model.post.findFirst({ + where: { + group: body.group, + deletedAt: null, + parentPostId: null, + }, + select: { + id: true, + }, + }) + )?.id! + : undefined; + + if (body.group) { + await this._post.model.post.updateMany({ + where: { + group: body.group, + deletedAt: null, + }, + data: { + parentPostId: null, + deletedAt: new Date(), + }, + }); + } + + return { previousPost, posts }; } } diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index f0f1aea1..e76fe0d6 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -45,7 +45,11 @@ export class PostsService { async getPost(orgId: string, id: string) { const posts = await this.getPostsRecursively(id, false, orgId); return { - posts, + group: posts?.[0]?.group, + posts: posts.map((post) => ({ + ...post, + image: JSON.parse(post.image || '[]'), + })), integration: posts[0].integrationId, settings: JSON.parse(posts[0].settings || '{}'), }; @@ -109,25 +113,40 @@ export class PostsService { await this._postRepository.updatePost(posts[0].id, postId, releaseURL); } + async deletePost(orgId: string, group: string) { + const post = await this._postRepository.deletePost(orgId, group); + if (post?.id) { + await this._workerServiceProducer.delete('post', post.id); + } + } + async createPost(orgId: string, body: CreatePostDto) { for (const post of body.posts) { - const posts = await this._postRepository.createOrUpdatePost( - orgId, - body.date, - post - ); + const { previousPost, posts } = + await this._postRepository.createOrUpdatePost( + body.type, + orgId, + body.date, + post + ); - await this._workerServiceProducer.delete('post', posts[0].id); - - this._workerServiceProducer.emit('post', { - id: posts[0].id, - options: { - delay: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'), - }, - payload: { - id: posts[0].id, - }, - }); + if (posts?.length) { + await this._workerServiceProducer.delete( + 'post', + previousPost ? previousPost : posts?.[0]?.id + ); + if (body.type === 'schedule') { + // this._workerServiceProducer.emit('post', { + // id: posts[0].id, + // options: { + // delay: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'), + // }, + // payload: { + // id: posts[0].id, + // }, + // }); + } + } } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 18c026b8..5ea45440 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -22,11 +22,8 @@ model Organization { github GitHub[] subscription Subscription? Integration Integration[] - tags Tag[] - postTags PostTag[] - postMedia PostMedia[] post Post[] - slots Slots[] + Comments Comments[] } model User { @@ -37,6 +34,7 @@ model User { providerId String? organizations UserOrganization[] timezone Int + comments Comments[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -52,6 +50,9 @@ model UserOrganization { role Role @default(USER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([organizationId]) + @@index([userId]) } model GitHub { @@ -66,6 +67,7 @@ model GitHub { updatedAt DateTime @updatedAt @@index([login]) + @@index([organizationId]) } model Trending { @@ -78,6 +80,7 @@ model Trending { updatedAt DateTime @updatedAt @@unique([language]) + @@index([hash]) } model TrendingLog { @@ -104,9 +107,10 @@ model Media { path String organization Organization @relation(fields: [organizationId], references: [id]) organizationId String - posts PostMedia[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([organizationId]) } model Subscription { @@ -120,6 +124,8 @@ model Subscription { period Period createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([organizationId]) } model Integration { @@ -139,46 +145,26 @@ model Integration { @@unique([organizationId, internalId]) } -model Tag { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - name String - posts PostTag[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} +model Comments { + id String @id @default(uuid()) + content String + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) + date DateTime + parentCommentId String? + parentComment Comments? @relation("parentCommentId", fields: [parentCommentId], references: [id]) + childrenComment Comments[] @relation("parentCommentId") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? -model PostTag { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - postId String - post Post @relation(fields: [postId], references: [id]) - tagId String - tag Tag @relation(fields: [tagId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model PostMedia { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - postId String - post Post @relation(fields: [postId], references: [id]) - mediaId String - media Media @relation(fields: [mediaId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Slots { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - day Int - time Int + @@index([createdAt]) + @@index([organizationId]) + @@index([date]) + @@index([userId]) + @@index([deletedAt]) } model Post { @@ -188,6 +174,7 @@ model Post { organizationId String integrationId String content String + group String organization Organization @relation(fields: [organizationId], references: [id]) integration Integration @relation(fields: [integrationId], references: [id]) title String? @@ -198,10 +185,17 @@ model Post { settings String? parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id]) childrenPost Post[] @relation("parentPostId") - tags PostTag[] - media PostMedia[] + image String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([group]) + @@index([deletedAt]) + @@index([publishDate]) + @@index([state]) + @@index([organizationId]) + @@index([parentPostId]) } enum State { diff --git a/libraries/nestjs-libraries/src/dtos/comments/add.comment.dto.ts b/libraries/nestjs-libraries/src/dtos/comments/add.comment.dto.ts new file mode 100644 index 00000000..65043433 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/comments/add.comment.dto.ts @@ -0,0 +1,4 @@ +export class AddCommentDto { + content: string; + date: string; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts index 4a063f25..c554402a 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts @@ -1,8 +1,13 @@ import { - ArrayMinSize, IsArray, IsDateString, IsDefined, IsOptional, IsString, ValidateNested, + ArrayMinSize, IsArray, IsDateString, IsDefined, IsIn, IsOptional, IsString, MinLength, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto'; +import {MediaDto} from "@gitroom/nestjs-libraries/dtos/media/media.dto"; +import {AllProvidersSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings"; +import {MediumSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto"; +import {HashnodeSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto"; +import {RedditSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto"; export class EmptySettings {} export class Integration { @@ -14,11 +19,18 @@ export class Integration { export class PostContent { @IsDefined() @IsString() + @MinLength(6) content: string; @IsOptional() @IsString() id: string; + + @IsArray() + @IsOptional() + @Type(() => MediaDto) + @ValidateNested({each: true}) + image: MediaDto[] } export class Post { @@ -34,17 +46,31 @@ export class Post { @ValidateNested({ each: true }) value: PostContent[]; + @IsOptional() + @IsString() + group: string; + + @ValidateNested() @Type(() => EmptySettings, { keepDiscriminatorProperty: false, discriminator: { property: '__type', - subTypes: [{ value: DevToSettingsDto, name: 'devto' }], + subTypes: [ + { value: DevToSettingsDto, name: 'devto' }, + { value: MediumSettingsDto, name: 'medium' }, + { value: HashnodeSettingsDto, name: 'hashnode' }, + { value: RedditSettingsDto, name: 'reddit' }, + ], }, }) - settings: DevToSettingsDto; + settings: AllProvidersSettings; } export class CreatePostDto { + @IsDefined() + @IsIn(['draft', 'schedule']) + type: 'draft' | 'schedule'; + @IsDefined() @IsDateString() date: string; diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts index 9caa68af..8801f4d9 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts @@ -1,2 +1,10 @@ -import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto"; -export type AllProvidersSettings = DevToSettingsDto; \ No newline at end of file +import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto'; +import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto'; +import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto'; +import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto'; + +export type AllProvidersSettings = + | DevToSettingsDto + | MediumSettingsDto + | HashnodeSettingsDto + | RedditSettingsDto; diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts new file mode 100644 index 00000000..b6bfbcd7 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/hashnode.settings.dto.ts @@ -0,0 +1,55 @@ +import { + ArrayMinSize, + IsArray, + IsDefined, + IsOptional, + IsString, + Matches, + MinLength, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; + +export class HashnodeTagsSettings { + @IsString() + value: string; + + @IsString() + label: string; +} + +export class HashnodeSettingsDto { + @IsString() + @MinLength(6) + @IsDefined() + title: string; + + @IsString() + @MinLength(2) + @IsOptional() + subtitle: string; + + @IsOptional() + @ValidateNested() + @Type(() => MediaDto) + main_image?: MediaDto; + + @IsOptional() + @IsString() + @Matches( + /^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, + { + message: 'Invalid URL', + } + ) + canonical?: string; + + @IsString() + @IsDefined() + publication?: string; + + @IsArray() + @ArrayMinSize(1) + tags: HashnodeTagsSettings[]; +} diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts new file mode 100644 index 00000000..86018772 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/medium.settings.dto.ts @@ -0,0 +1,37 @@ +import {ArrayMaxSize, IsArray, IsDefined, IsOptional, IsString, Matches, MinLength, ValidateNested} from "class-validator"; + +export class MediumTagsSettings { + @IsString() + value: string; + + @IsString() + label: string; +} + +export class MediumSettingsDto { + @IsString() + @MinLength(2) + @IsDefined() + title: string; + + @IsString() + @MinLength(2) + @IsDefined() + subtitle: string; + + @IsOptional() + @IsString() + @Matches(/^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, { + message: 'Invalid URL' + }) + canonical?: string; + + @IsString() + @IsOptional() + publication?: string; + + @IsArray() + @ArrayMaxSize(4) + @IsOptional() + tags: MediumTagsSettings[]; +} \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts new file mode 100644 index 00000000..28d12d7b --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/reddit.dto.ts @@ -0,0 +1,73 @@ +import { + ArrayMinSize, + IsBoolean, + IsDefined, + IsString, + IsUrl, + MinLength, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; +import { Type } from 'class-transformer'; + +export class RedditFlairDto { + @IsString() + @IsDefined() + id: string; + + @IsString() + @IsDefined() + name: string; +} + +export class RedditSettingsDtoInner { + @IsString() + @MinLength(2) + @IsDefined() + subreddit: string; + + @IsString() + @MinLength(2) + @IsDefined() + title: string; + + @IsString() + @MinLength(2) + @IsDefined() + type: string; + + @ValidateIf((e) => e.type === 'link') + @IsUrl() + @IsDefined() + url: string; + + @IsBoolean() + @IsDefined() + is_flair_required: boolean; + + @ValidateIf((e) => e.is_flair_required) + @IsDefined() + @ValidateNested() + flair: RedditFlairDto; + + @ValidateIf((e) => e.type === 'media') + @ValidateNested({ each: true }) + @Type(() => MediaDto) + @ArrayMinSize(1) + media: MediaDto[]; +} + +export class RedditSettingsValueDto { + @Type(() => RedditSettingsDtoInner) + @IsDefined() + @ValidateNested() + value: RedditSettingsDtoInner; +} + +export class RedditSettingsDto { + @Type(() => RedditSettingsValueDto) + @ValidateNested({ each: true }) + @ArrayMinSize(1) + subreddit: RedditSettingsValueDto[]; +} diff --git a/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts b/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts index 73bc245e..bf2fceb6 100644 --- a/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/hashnode.provider.ts @@ -1,18 +1,26 @@ -import {ArticleIntegrationsInterface, ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface"; +import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface'; +import { tags } from '@gitroom/nestjs-libraries/integrations/article/hashnode.tags'; +import { jsonToGraphQLQuery } from 'json-to-graphql-query'; +import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto'; export class HashnodeProvider implements ArticleProvider { - identifier = 'hashnode'; - name = 'Hashnode'; - async authenticate(token: string) { - try { - const {data: {me: {name, id, profilePicture}}} = await (await fetch('https://gql.hashnode.com', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `${token}` - }, - body: JSON.stringify({ - query: ` + identifier = 'hashnode'; + name = 'Hashnode'; + async authenticate(token: string) { + try { + const { + data: { + me: { name, id, profilePicture }, + }, + } = await ( + await fetch('https://gql.hashnode.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + body: JSON.stringify({ + query: ` query { me { name, @@ -20,28 +28,125 @@ export class HashnodeProvider implements ArticleProvider { profilePicture } } - ` - }) - })).json(); + `, + }), + }) + ).json(); - return { - id, name, token, picture: profilePicture - } - } - catch (err) { - return { - id: '', - name: '', - token: '', - picture: '' - } - } + return { + id, + name, + token, + picture: profilePicture, + }; + } catch (err) { + return { + id: '', + name: '', + token: '', + picture: '', + }; } + } - async post(token: string, content: string, settings: object) { - return { - postId: '123', - releaseURL: 'https://dev.to' - } - } -} \ No newline at end of file + async tags() { + return tags.map((tag) => ({ value: tag.objectID, label: tag.name })); + } + + async publications(token: string) { + const { + data: { + me: { + publications: { edges }, + }, + }, + } = await ( + await fetch('https://gql.hashnode.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + body: JSON.stringify({ + query: ` + query { + me { + publications (first: 50) { + edges{ + node { + id + title + } + } + } + } + } + `, + }), + }) + ).json(); + + return edges.map( + ({ node: { id, title } }: { node: { id: string; title: string } }) => ({ + id, + name: title, + }) + ); + } + + async post(token: string, content: string, settings: HashnodeSettingsDto) { + const query = jsonToGraphQLQuery({ + mutation: { + publishPost: { + __args: { + input: { + title: settings.title, + publicationId: settings.publication, + ...(settings.canonical + ? { originalArticleURL: settings.canonical } + : {}), + contentMarkdown: content, + tags: settings.tags.map((tag) => ({ id: tag.value })), + ...(settings.subtitle ? { subtitle: settings.subtitle } : {}), + ...(settings.main_image + ? { + coverImageOptions: { + coverImageURL: `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${settings?.main_image?.path}`, + }, + } + : {}), + }, + }, + post: { + id: true, + url: true, + }, + }, + }, + }, {pretty: true}); + + const { + data: { + publishPost: { + post: { id, url }, + }, + }, + } = await ( + await fetch('https://gql.hashnode.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `${token}`, + }, + body: JSON.stringify({ + query, + }), + }) + ).json(); + + return { + postId: id, + releaseURL: url, + }; + } +} diff --git a/libraries/nestjs-libraries/src/integrations/article/hashnode.tags.ts b/libraries/nestjs-libraries/src/integrations/article/hashnode.tags.ts new file mode 100644 index 00000000..9bb08efd --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/article/hashnode.tags.ts @@ -0,0 +1,5194 @@ +export const tags = [ + { + "name": "JavaScript", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513320898547/BJjpblWfG.png", + "slug": "javascript", + "objectID": "56744721958ef13879b94cad" + }, + { + "name": "General Programming", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1535648192079/H1daWiBvQ.png", + "slug": "programming", + "objectID": "56744721958ef13879b94c7e" + }, + { + "name": "React", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513321478077/ByCWNxZMf.png", + "slug": "reactjs", + "objectID": "56744723958ef13879b95434" + }, + { + "name": "Web Development", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450469658/vdxecajl3uwbprclsctm.jpg", + "slug": "web-development", + "objectID": "56744722958ef13879b94f1b" + }, + { + "name": "Python", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512408213/rJeQpSNIX.png", + "slug": "python", + "objectID": "56744721958ef13879b94d67" + }, + { + "name": "Node.js", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513321388034/SJV3QgWfz.png", + "slug": "nodejs", + "objectID": "56744722958ef13879b94ffb" + }, + { + "name": "CSS", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513316949083/By6UMkbfG.png", + "slug": "css", + "objectID": "56744721958ef13879b94b91" + }, + { + "name": "beginners", + "slug": "beginners", + "objectID": "56744723958ef13879b955a9" + }, + { + "name": "Java", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512378322/H1gM-pH4UQ.png", + "slug": "java", + "objectID": "56744721958ef13879b94c9f" + }, + { + "name": "Developer", + "slug": "developer", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1554321431158/MqVqSHr8Q.jpeg", + "objectID": "56744723958ef13879b952d7" + }, + { + "name": "HTML5", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513322217442/SkZlDeWzz.png", + "slug": "html5", + "objectID": "56744723958ef13879b95483" + }, + { + "name": "2Articles1Week", + "slug": "2articles1week", + "logo": "", + "objectID": "5f058ab0c9763d47e2d2eedc" + }, + { + "name": "learning", + "slug": "learning", + "objectID": "56744723958ef13879b9532b" + }, + { + "name": "PHP", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513177307594/rJ4Jba0-G.png", + "slug": "php", + "objectID": "56744722958ef13879b94fd9" + }, + { + "name": "AWS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468151/vmrnzobr1lonnigttn3c.png", + "slug": "aws", + "objectID": "56744721958ef13879b94bc5" + }, + { + "name": "Tutorial", + "slug": "tutorial", + "objectID": "56744720958ef13879b947ce" + }, + { + "name": "programming blogs", + "slug": "programming-blogs", + "objectID": "56744721958ef13879b94ae7" + }, + { + "name": "coding", + "slug": "coding", + "objectID": "56744723958ef13879b954c1" + }, + { + "name": "Go Language", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512687168/S1D40rVLm.png", + "slug": "go", + "objectID": "56744721958ef13879b94bd0" + }, + { + "name": "Frontend Development", + "slug": "frontend-development", + "objectID": "56a399f292921b8f79d3633c" + }, + { + "name": "GitHub", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513321555902/BkhLElZMG.png", + "slug": "github", + "objectID": "56744721958ef13879b94c63" + }, + { + "name": "Hashnode", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1619605440273/S3_X4Rf7V.jpeg", + "slug": "hashnode", + "objectID": "567ae5a72b926c3063c3061a" + }, + { + "name": "Python 3", + "slug": "python3", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1503468096/axvqxfbcm0b7ourhshj7.jpg", + "objectID": "56744723958ef13879b95342" + }, + { + "name": "Codenewbies", + "slug": "codenewbies", + "objectID": "5f22b52283e4e9440619af83" + }, + { + "name": "webdev", + "slug": "webdev", + "objectID": "56744723958ef13879b952af" + }, + { + "name": "Machine Learning", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513321644252/Sk43El-fz.png", + "slug": "machine-learning", + "objectID": "56744722958ef13879b950a8" + }, + { + "name": "General Advice", + "slug": "general-advice", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516183731966/B13heohVM.jpeg", + "objectID": "56fe3b2e7a82968f9f7d51c1" + }, + { + "name": "software development", + "slug": "software-development", + "objectID": "56744721958ef13879b94ad1" + }, + { + "name": "CSS3", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513316988840/r1Htz1Wzz.png", + "slug": "css3", + "objectID": "56744721958ef13879b94b21" + }, + { + "name": "Android", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468271/qbj34hxd8981nfdugyph.png", + "slug": "android", + "objectID": "56744723958ef13879b953d0" + }, + { + "name": "Productivity", + "slug": "productivity", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1497250361/v3sij4jc8hz9xoic22eq.png", + "objectID": "56744721958ef13879b94a60" + }, + { + "name": "React Native", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475235386/rkij45wit50lfpkbte5q.jpg", + "slug": "react-native", + "objectID": "56744722958ef13879b94f4d" + }, + { + "name": "100DaysOfCode", + "slug": "100daysofcode", + "objectID": "576ab68f152618ad1dc938ad" + }, + { + "name": "Design", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513324674454/r1qtxW-zf.png", + "slug": "design", + "objectID": "56744722958ef13879b94e89" + }, + { + "name": "Devops", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496913014/cnvm0znfqcrwelhgtblb.png", + "slug": "devops", + "objectID": "56744723958ef13879b9550d" + }, + { + "name": "Open Source", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496913431/hdg1q4zbmobhrq0csomm.png", + "slug": "opensource", + "objectID": "56744722958ef13879b94f32" + }, + { + "name": "Git", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1473706112/l2hom2y5xxpgwlgg0sz0.jpg", + "slug": "git", + "objectID": "56744723958ef13879b9526c" + }, + { + "name": "HTML", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513322147587/Hk2jIxZGG.png", + "slug": "html", + "objectID": "56744722958ef13879b94f96" + }, + { + "name": "data science", + "slug": "data-science", + "objectID": "56744721958ef13879b94e35" + }, + { + "name": "Testing", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450619295/xszq3zb8t6rmgg6regon.png", + "slug": "testing", + "objectID": "56744723958ef13879b9549b" + }, + { + "name": "Linux", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450641462/ogpsvoxw5kt8aksuiptj.png", + "slug": "linux", + "objectID": "56744721958ef13879b94b55" + }, + { + "name": "Security", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472744837/bnzk4gvspiy66dsmw9ku.png", + "slug": "security", + "objectID": "56744722958ef13879b94fb7" + }, + { + "name": "Laravel", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1454754733/exjubzyuvwz0pvvpxxwv.jpg", + "slug": "laravel", + "objectID": "56744721958ef13879b94a83" + }, + { + "name": "TypeScript", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1470054384/fuy3ypcjuj4cwdz4qpxn.jpg", + "slug": "typescript", + "objectID": "56744723958ef13879b954e0" + }, + { + "name": "APIs", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468334/jirjz7cc54l2mstzpaab.png", + "slug": "apis", + "objectID": "56744723958ef13879b95245" + }, + { + "name": "Ruby", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512722989/BksL0SELm.png", + "slug": "ruby", + "objectID": "56744721958ef13879b94c0a" + }, + { + "name": "Vue.js", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1505294440/t5igqu22z1s86xa7nkqi.png", + "slug": "vuejs", + "objectID": "56744722958ef13879b950e4" + }, + { + "name": "technology", + "slug": "technology", + "objectID": "56744721958ef13879b94d26" + }, + { + "name": "Docker", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1453789075/ryxk99vk41tdn8bo28m4.png", + "slug": "docker", + "objectID": "56744721958ef13879b94b77" + }, + { + "name": "programming languages", + "slug": "programming-languages", + "objectID": "579a67e2cec33eafc07249c7" + }, + { + "name": "Programming Tips", + "slug": "programming-tips", + "objectID": "5f398753c4d5973f55c912fb" + }, + { + "name": "Cloud", + "slug": "cloud", + "objectID": "56744721958ef13879b94938" + }, + { + "name": "Blogging", + "slug": "blogging", + "objectID": "56744721958ef13879b949aa" + }, + { + "name": "newbie", + "slug": "newbie", + "objectID": "56744720958ef13879b947e8" + }, + { + "name": "Career", + "slug": "career", + "objectID": "56aa13e5f28f9d9d99e3a5de" + }, + { + "name": "Swift", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512662717/Sy1XAHNLQ.png", + "slug": "swift", + "objectID": "56744722958ef13879b94ead" + }, + { + "name": "Flutter Community", + "slug": "flutter", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1560841840250/KhofPXnAk.jpeg", + "objectID": "56744722958ef13879b9507c" + }, + { + "name": "python beginner", + "slug": "python-beginner", + "objectID": "5f3867d1c4d5973f55c90b8b" + }, + { + "name": "Software Engineering", + "slug": "software-engineering", + "objectID": "569d22c892921b8f79d35f68" + }, + { + "name": "learn coding", + "slug": "learn-coding", + "objectID": "5f3f40bfdfbb4247f7c14d4c" + }, + { + "name": "MongoDB", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450467711/awgzya1xei3pgch5b8xu.png", + "slug": "mongodb", + "objectID": "56744722958ef13879b94f6f" + }, + { + "name": "iOS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468231/t4x2aoglmhhz9yw3ezry.png", + "slug": "ios", + "objectID": "56744722958ef13879b94f11" + }, + { + "name": "algorithms", + "slug": "algorithms", + "objectID": "56744721958ef13879b94a8d" + }, + { + "name": "Web Design", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450622407/deczahnypldw1ftbdxog.png", + "slug": "web-design", + "objectID": "56744721958ef13879b94d32" + }, + { + "name": "Databases", + "slug": "databases", + "objectID": "56744722958ef13879b950eb" + }, + { + "name": "ES6", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512767931/S1dYCSNIm.png", + "slug": "es6", + "objectID": "56744723958ef13879b954cb" + }, + { + "name": "Learning Journey", + "slug": "learning-journey", + "objectID": "5f9435c7fbdce372c9a56fb6" + }, + { + "name": "Blockchain", + "slug": "blockchain", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1540281064342/rkle7U3sQ.png", + "objectID": "5690224191716a2d1dbadbc1" + }, + { + "name": "data structures", + "slug": "data-structures", + "objectID": "56744722958ef13879b951bb" + }, + { + "name": "Redux", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513322046756/HyPSUgWMG.png", + "slug": "redux", + "objectID": "56744723958ef13879b95567" + }, + { + "name": "backend", + "slug": "backend", + "objectID": "56744722958ef13879b950bd" + }, + { + "name": "C#", + "slug": "csharp", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512595400/HkoATH48Q.png", + "objectID": "56744721958ef13879b94a30" + }, + { + "name": "Startups", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1459504275/iksgbnwvscz6zjzk5nhe.jpg", + "slug": "startups", + "objectID": "56744721958ef13879b94b5b" + }, + { + "name": "Django", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475235489/g7q2vh5igqcxo8jlfwl9.jpg", + "slug": "django", + "objectID": "56744722958ef13879b94e81" + }, + { + "name": "UX", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1474023086/dnrwfr6sxylhx60mp26j.png", + "slug": "ux", + "objectID": "56744722958ef13879b94e9d" + }, + { + "name": "interview", + "slug": "interview", + "objectID": "56744720958ef13879b947e1" + }, + { + "name": "Visual Studio Code", + "slug": "vscode", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1497045716/r3myqwr6m8olahqaxl5x.png", + "objectID": "57323a8bae9d49b5a5a5b39c" + }, + { + "name": "internships", + "slug": "internships", + "objectID": "56744720958ef13879b94811" + }, + { + "name": "Next.js", + "slug": "nextjs", + "objectID": "584879f0c0aaf085e2012086" + }, + { + "name": "Kubernetes", + "slug": "kubernetes", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1554318943530/J-r4NJeEi.png", + "objectID": "56744723958ef13879b9522c" + }, + { + "name": "Computer Science", + "slug": "computer-science", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1514959838703/BkDJVxqQM.jpeg", + "objectID": "56744722958ef13879b9512b" + }, + { + "name": "REST API", + "slug": "rest-api", + "objectID": "56b1208d04f0061506b360ff" + }, + { + "name": "business", + "slug": "business", + "objectID": "56744723958ef13879b952a1" + }, + { + "name": "automation", + "slug": "automation", + "objectID": "56744723958ef13879b9535d" + }, + { + "name": "Kotlin", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1458728299/fuo7n9epkkxyafihrlhz.jpg", + "slug": "kotlin", + "objectID": "56c2f39e850906a7da47cdeb" + }, + { + "name": "Google", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450469897/djpesw0ajrbxvmlyoezx.png", + "slug": "google", + "objectID": "56744723958ef13879b95470" + }, + { + "name": "app development", + "slug": "app-development", + "objectID": "56744720958ef13879b947c4" + }, + { + "name": "Azure", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1524473475544/B1ntAzsnM.jpeg", + "slug": "azure", + "objectID": "56744721958ef13879b94d89" + }, + { + "name": "Game Development", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1473275923/lhhroyopcm9gpfvqxe44.jpg", + "slug": "game-development", + "objectID": "56744723958ef13879b953f2" + }, + { + "name": "C++", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512626199/BkcgCSNUm.png", + "slug": "cpp", + "objectID": "56744721958ef13879b948b7" + }, + { + "name": "js", + "slug": "js", + "objectID": "56744721958ef13879b94bf5" + }, + { + "name": "UI", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1487144606/jy8ee18buuag2zbsbqai.png", + "slug": "ui", + "objectID": "56744723958ef13879b954f5" + }, + { + "name": "Mobile Development", + "slug": "mobile-development", + "objectID": "568a9b8ce4c4e23aef243c1f" + }, + { + "name": "Cloud Computing", + "slug": "cloud-computing", + "objectID": "56744723958ef13879b9533a" + }, + { + "name": "frontend", + "slug": "frontend", + "objectID": "56744721958ef13879b94d0f" + }, + { + "name": "Artificial Intelligence", + "slug": "artificial-intelligence", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496737518/sgflljcm3hidlvipsriq.png", + "objectID": "56744721958ef13879b94927" + }, + { + "name": "npm", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1460372304/ovff2sszokeskrwdfjjv.png", + "slug": "npm", + "objectID": "56744723958ef13879b95322" + }, + { + "name": "development", + "slug": "development", + "objectID": "56744721958ef13879b94d9b" + }, + { + "name": "Ruby on Rails", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475235552/twnpcxcm29mub2gez4yf.jpg", + "slug": "ruby-on-rails", + "objectID": "56744722958ef13879b94ff1" + }, + { + "name": "WordPress", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450732925/vndlqh4zwgqoy6kbcs0j.jpg", + "slug": "wordpress", + "objectID": "56744721958ef13879b94beb" + }, + { + "name": "tips", + "slug": "tips", + "objectID": "56744723958ef13879b95319" + }, + { + "name": "javascript framework", + "slug": "javascript-framework", + "objectID": "56744723958ef13879b95527" + }, + { + "name": "Technical writing ", + "slug": "technical-writing-1", + "objectID": "5f3330322a23d9080d17a0da" + }, + { + "name": "Express", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513936680781/HJbEP8qzM.png", + "slug": "express", + "objectID": "56744721958ef13879b9487d" + }, + { + "name": "serverless", + "slug": "serverless", + "objectID": "57979f8dcec33eafc07247a2" + }, + { + "name": "Angular", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450469536/svgqrg8jtoqihqdffiai.jpg", + "slug": "angular", + "objectID": "56744722958ef13879b94f59" + }, + { + "name": "DevLife", + "slug": "devlife", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516175805632/HkL6WK3Ez.jpeg", + "objectID": "592fe1bf8515388d7dfc2650" + }, + { + "name": "Functional Programming", + "slug": "functional-programming", + "objectID": "568f5c6beea132481d017c36" + }, + { + "name": "programmer", + "slug": "programmer", + "objectID": "568409636b179c61d167f05d" + }, + { + "name": "python projects", + "slug": "python-projects", + "objectID": "5f76046e37eb052c1b80da9f" + }, + { + "name": "MySQL", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496912606/hclufcmqr2btz24a6egj.png", + "slug": "mysql", + "objectID": "56744721958ef13879b94dff" + }, + { + "name": "Dart", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450601337/v7ng3klyzehzxtbjoym9.png", + "slug": "dart", + "objectID": "56744721958ef13879b94df0" + }, + { + "name": "Firebase", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1464240463/xo1rbiqimh25bmlgwb3g.jpg", + "slug": "firebase", + "objectID": "56744722958ef13879b94e99" + }, + { + "name": "Windows", + "slug": "windows", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1554134710664/jnLVaVy-N.png", + "objectID": "56744723958ef13879b953f7" + }, + { + "name": "code", + "slug": "code", + "objectID": "56744721958ef13879b94982" + }, + { + "name": "GraphQL", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475235506/qbofja8kwx8cw8nuyaqg.jpg", + "slug": "graphql", + "objectID": "56744723958ef13879b9555c" + }, + { + "name": "SEO", + "slug": "seo", + "objectID": "56744722958ef13879b9519c" + }, + { + "name": "ReactHooks", + "slug": "reacthooks", + "objectID": "5f8523be6ad92638db4944a9" + }, + { + "name": "#beginners #learningtocode #100daysofcode", + "slug": "beginners-learningtocode-100daysofcode", + "objectID": "5f789ec19c3b6e410121699a" + }, + { + "name": "hacking", + "slug": "hacking", + "objectID": "56744723958ef13879b9553a" + }, + { + "name": "Cryptocurrency", + "slug": "cryptocurrency", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1512988849862/SJ5heynZG.png", + "objectID": "58e4c1144d64a3de3e94b31b" + }, + { + "name": "SQL", + "slug": "sql", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1501338352/cv7owxtxvr39rjzxoolr.png", + "objectID": "56744723958ef13879b953ed" + }, + { + "name": "hashnodebootcamp", + "slug": "hashnodebootcamp", + "objectID": "5f75f322b7a1d82bf9b34c6d" + }, + { + "name": "Tailwind CSS", + "slug": "tailwind-css", + "objectID": "5f4ebbb150b5c61ec6ef4ad2" + }, + { + "name": "webpack", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1457865805/st9hz4f5ufmpxhizmfpk.jpg", + "slug": "webpack", + "objectID": "56744722958ef13879b95055" + }, + { + "name": "learn", + "slug": "learn", + "objectID": "56a2235672ca04ea5d7a00c2" + }, + { + "name": "first post", + "slug": "first-post-1", + "objectID": "5f08ee681981c53c4987f2b3" + }, + { + "name": "design patterns", + "slug": "design-patterns", + "objectID": "56744721958ef13879b94968" + }, + { + "name": "ai", + "slug": "ai", + "objectID": "56744721958ef13879b9488e" + }, + { + "name": "Microservices", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1479724330/cpcqfxm9af8d8esgo8wp.jpg", + "slug": "microservices", + "objectID": "56744721958ef13879b948a2" + }, + { + "name": "data analysis", + "slug": "data-analysis", + "objectID": "56744722958ef13879b951ac" + }, + { + "name": "best practices", + "slug": "best-practices", + "objectID": "56744723958ef13879b95598" + }, + { + "name": "beginner", + "slug": "beginner", + "objectID": "56744723958ef13879b952b6" + }, + { + "name": "Deep Learning", + "slug": "deep-learning", + "objectID": "578f611523e94ba91a5bebd8" + }, + { + "name": "Ubuntu", + "slug": "ubuntu", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496151690/x8g04hsjiekjgkkuhrk7.png", + "objectID": "56744721958ef13879b94988" + }, + { + "name": "C", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475235467/zfbdpx1pe00glfy6lc6b.jpg", + "slug": "c", + "objectID": "56744721958ef13879b9492c" + }, + { + "name": "MERN Stack", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512793459/Hk-s0B4Im.png", + "slug": "mern", + "objectID": "56c32d8c316f8ee15e9e0fde" + }, + { + "name": "education", + "slug": "education", + "objectID": "56b631c8e6740d0959b6f3ef" + }, + { + "name": "authentication", + "slug": "authentication", + "objectID": "56744721958ef13879b94b00" + }, + { + "name": "community", + "slug": "community", + "objectID": "56744722958ef13879b9514c" + }, + { + "name": "marketing", + "slug": "marketing", + "objectID": "57449fa89ade925885158d1e" + }, + { + "name": "Hello World", + "slug": "hello-world", + "objectID": "591d0f67b5bbb96606f07af4" + }, + { + "name": "tools", + "slug": "tools", + "objectID": "56744721958ef13879b94e0c" + }, + { + "name": "ecommerce", + "slug": "ecommerce", + "objectID": "56744722958ef13879b95041" + }, + { + "name": "news", + "slug": "news", + "objectID": "56744721958ef13879b9493e" + }, + { + "name": "Microsoft", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450622053/ioqfwklxmzqwwy7jrxmj.png", + "slug": "microsoft", + "objectID": "56744721958ef13879b94d1d" + }, + { + "name": "jQuery", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450815745/cd8sl0j2hkeuoq2isuc3.png", + "slug": "jquery", + "objectID": "56744721958ef13879b94c2b" + }, + { + "name": "Javascript library", + "slug": "javascript-library", + "objectID": "568fa207525da8063d08fb68" + }, + { + "name": "data", + "slug": "data", + "objectID": "56744721958ef13879b949d3" + }, + { + "name": "clean code", + "slug": "clean-code", + "objectID": "573504d39835efadc8742016" + }, + { + "name": "web", + "slug": "web", + "objectID": "56744722958ef13879b94f40" + }, + { + "name": "programing", + "slug": "programing", + "objectID": "56ab1a78f28f9d9d99e3a6d1" + }, + { + "name": "tech ", + "slug": "tech", + "objectID": "5677de7c7dd5d4174dcc2073" + }, + { + "name": "Mobile apps", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450619495/qiwhbnoxoas2b5dtb6cx.png", + "slug": "mobile-apps", + "objectID": "56744721958ef13879b94c5b" + }, + { + "name": "performance", + "slug": "performance", + "objectID": "56744721958ef13879b94dc4" + }, + { + "name": "UI Design", + "slug": "ui-design", + "objectID": "5682df44aeae5c9e229cf9f9" + }, + { + "name": "PostgreSQL", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1460706552/iwl62ldvrzgf4k9rhame.jpg", + "slug": "postgresql", + "objectID": "56744721958ef13879b949b5" + }, + { + "name": "Rust", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512703511/HJDSCr4UQ.png", + "slug": "rust", + "objectID": "5684bee6bf03be7d4a9ed853" + }, + { + "name": "motivation", + "slug": "motivation", + "objectID": "56b0ba4604f0061506b35fae" + }, + { + "name": "software architecture", + "slug": "software-architecture", + "objectID": "56744722958ef13879b950c9" + }, + { + "name": "introduction", + "slug": "introduction", + "objectID": "56744721958ef13879b948cc" + }, + { + "name": "Bootstrap", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450470158/wpi0t8fj9kr8on9v6jmd.jpg", + "slug": "bootstrap", + "objectID": "56744721958ef13879b94be1" + }, + { + "name": "networking", + "slug": "networking", + "objectID": "56ffbb5d5861692778050361" + }, + { + "name": "blog", + "slug": "blog", + "objectID": "56744721958ef13879b948ac" + }, + { + "name": "jobs", + "slug": "jobs", + "objectID": "56a77939281161e11972fdd7" + }, + { + "name": "terminal", + "slug": "terminal", + "objectID": "56744721958ef13879b94da6" + }, + { + "name": "command line", + "slug": "command-line", + "objectID": "56744723958ef13879b9539a" + }, + { + "name": "website", + "slug": "website", + "objectID": "5674471d958ef13879b94785" + }, + { + "name": "Developer Tools", + "slug": "developer-tools", + "objectID": "57ebac0bd9b08ec06a77be05" + }, + { + "name": "aws lambda", + "slug": "aws-lambda", + "objectID": "57c7ea36e53060955aa8c0c0" + }, + { + "name": "Ethereum", + "slug": "ethereum", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1512988945062/HJKfZJhWf.png", + "objectID": "58e4c1144d64a3de3e94b31d" + }, + { + "name": "System Architecture", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496913335/duppaieikvyvepmoj6uz.png", + "slug": "system-architecture", + "objectID": "56744723958ef13879b955b0" + }, + { + "name": "#cybersecurity", + "slug": "cybersecurity-1", + "objectID": "5f2e70c0b8ac395b1f23a6cb" + }, + { + "name": "linux for beginners", + "slug": "linux-for-beginners", + "objectID": "5fa5022a3e634314b5179cf5" + }, + { + "name": "Games", + "slug": "games", + "objectID": "578f6a105460288cdeb6f2ab" + }, + { + "name": "developers", + "slug": "developers", + "objectID": "56744722958ef13879b94f05" + }, + { + "name": "internet", + "slug": "internet", + "objectID": "56f260f15ec781bb472f83af" + }, + { + "name": "android app development", + "slug": "android-app-development", + "objectID": "56744721958ef13879b94890" + }, + { + "name": "full stack", + "slug": "full-stack", + "objectID": "56744723958ef13879b95387" + }, + { + "name": "server", + "slug": "server", + "objectID": "56744721958ef13879b94e17" + }, + { + "name": "projects", + "slug": "projects", + "objectID": "56744722958ef13879b95074" + }, + { + "name": "macOS", + "slug": "macos", + "objectID": "576a1d6e13cc2eb2d90e2383" + }, + { + "name": "project management", + "slug": "project-management", + "objectID": "569d22af46dfdb8479aa6921" + }, + { + "name": "writing", + "slug": "writing", + "objectID": "5674471d958ef13879b9477e" + }, + { + "name": "Flutter Examples", + "slug": "flutter-examples", + "objectID": "5f08f6a1b0bf5b3c273ea78b" + }, + { + "name": "guide", + "slug": "guide", + "objectID": "56744723958ef13879b955a7" + }, + { + "name": "deployment", + "slug": "deployment", + "objectID": "56744721958ef13879b94dad" + }, + { + "name": "array", + "slug": "array", + "objectID": "578e290c5460288cdeb6f187" + }, + { + "name": "Bash", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1464705930/i6dyhkbiwqezfwbsq4c2.jpg", + "slug": "bash", + "objectID": "56744722958ef13879b95119" + }, + { + "name": "Bitcoin", + "slug": "bitcoin", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1512988974934/rJwNWJhZz.png", + "objectID": "5697e90f46dfdb8479aa6708" + }, + { + "name": "Google Chrome", + "slug": "chrome", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502183259/uhcfgovkcf3pm66xjsl0.png", + "objectID": "56744722958ef13879b94f68" + }, + { + "name": ".NET", + "slug": "net", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1515075179602/rkEdLho7G.jpeg", + "objectID": "56744723958ef13879b9556e" + }, + { + "name": "dotnet", + "slug": "dotnet", + "objectID": "5794f65abecb9ebac0d5fc55" + }, + { + "name": "life", + "slug": "life", + "objectID": "57bc257693309a25047c5e43" + }, + { + "name": "Twitter", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1464240092/ysal5yejuviop7p7bvbl.png", + "slug": "twitter", + "objectID": "56744721958ef13879b949ad" + }, + { + "name": "Object Oriented Programming", + "slug": "object-oriented-programming", + "objectID": "591e9732ab184fdc3bcd9185" + }, + { + "name": "iot", + "slug": "iot", + "objectID": "56744723958ef13879b9532f" + }, + { + "name": "json", + "slug": "json", + "objectID": "56744721958ef13879b94dec" + }, + { + "name": "api", + "slug": "api", + "objectID": "56744721958ef13879b94c20" + }, + { + "name": "Express.js", + "slug": "expressjs-cilb5apda0066e053g7td7q24", + "objectID": "56d729602c0ee8a839b966f1" + }, + { + "name": "basics", + "slug": "basics", + "objectID": "57b75ddd51da93ffde24c7d9" + }, + { + "name": "http", + "slug": "http", + "objectID": "56744721958ef13879b94c04" + }, + { + "name": "Self Improvement ", + "slug": "self-improvement-1", + "objectID": "5f2e55763b12e25afe3e4d05" + }, + { + "name": "GitLab", + "slug": "gitlab", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1506019761/umvea3aqsquj7z9ighjt.png", + "objectID": "56bb10616bd8ce129b0bcc6c" + }, + { + "name": "google cloud", + "slug": "google-cloud", + "objectID": "56744722958ef13879b951dd" + }, + { + "name": "Spring", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1458677282/mtezd0wf8jhhmbgkzo1g.jpg", + "slug": "spring", + "objectID": "5674471d958ef13879b94772" + }, + { + "name": "selenium", + "slug": "selenium", + "objectID": "56a1bb2a92921b8f79d3620f" + }, + { + "name": "Gatsby", + "slug": "gatsby", + "objectID": "58a37012803129b7f158f514" + }, + { + "name": "containers", + "slug": "containers", + "objectID": "571f798917ae2452d9887631" + }, + { + "name": "resources", + "slug": "resources", + "objectID": "56744721958ef13879b94d55" + }, + { + "name": "operating system", + "slug": "operating-system", + "objectID": "56744721958ef13879b94b09" + }, + { + "name": "product", + "slug": "product", + "objectID": "577f7bc442d3fa70a37e450e" + }, + { + "name": "cms", + "slug": "cms", + "objectID": "56744723958ef13879b953ff" + }, + { + "name": "ui ux designer", + "slug": "ui-ux-designer", + "objectID": "5f7af8bd9c3b6e4101218399" + }, + { + "name": "hosting", + "slug": "hosting", + "objectID": "56744721958ef13879b94b0f" + }, + { + "name": "social media", + "slug": "social-media", + "objectID": "5775ff2c57675ec2fcfd086e" + }, + { + "name": "debugging", + "slug": "debugging", + "objectID": "56744723958ef13879b95372" + }, + { + "name": "Heroku", + "slug": "heroku", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496175418/k6pahvykel6hcfqtkk3d.jpg", + "objectID": "568935c69a4538cecc3ae55f" + }, + { + "name": "software", + "slug": "software", + "objectID": "56744721958ef13879b9481e" + }, + { + "name": "asp.net core", + "slug": "aspnet-core", + "objectID": "56bad3b76bd8ce129b0bcc04" + }, + { + "name": "hackathon", + "slug": "hackathon", + "objectID": "56744720958ef13879b947d4" + }, + { + "name": "framework", + "slug": "framework", + "objectID": "56744721958ef13879b94b4d" + }, + { + "name": "cli", + "slug": "cli", + "objectID": "56744723958ef13879b953a7" + }, + { + "name": "array methods", + "slug": "array-methods", + "objectID": "5f397a30c4d5973f55c91219" + }, + { + "name": "Electron", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1473241164/mqhaydhn8fhejzrsxofr.png", + "slug": "electron", + "objectID": "56744723958ef13879b95419" + }, + { + "name": "challenge", + "slug": "challenge", + "objectID": "56744721958ef13879b949c9" + }, + { + "name": "Freelancing", + "slug": "freelancing", + "objectID": "56744723958ef13879b953cc" + }, + { + "name": "linux-basics", + "slug": "linux-basics", + "objectID": "5fb01c1fc03b0e471014f758" + }, + { + "name": "portfolio", + "slug": "portfolio", + "objectID": "5690e78091716a2d1dbadc0f" + }, + { + "name": "functions", + "slug": "functions", + "objectID": "56744721958ef13879b94a01" + }, + { + "name": "Springboot", + "slug": "springboot", + "objectID": "58646144cc0caec55e2fd1d1" + }, + { + "name": "youtube", + "slug": "youtube", + "objectID": "56ced112f0ec33085f1cc5ab" + }, + { + "name": "Browsers", + "slug": "browsers", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502183382/psbzjcrqxjjndian3nph.png", + "objectID": "56744721958ef13879b94d63" + }, + { + "name": "vue", + "slug": "vue", + "objectID": "570e5021115103c3b09785e1" + }, + { + "name": "Flask Framework", + "slug": "flask", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1518503935975/S1_-_WePM.png", + "objectID": "56744723958ef13879b95588" + }, + { + "name": "HashnodeCommunity", + "slug": "hashnodecommunity", + "objectID": "5f3272264332ee07eb55c4bd" + }, + { + "name": "Apple", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1465890893/iickievhb3ymoyga6wyw.png", + "slug": "apple", + "objectID": "56744721958ef13879b948ba" + }, + { + "name": "Business and Finance ", + "slug": "business-and-finance", + "objectID": "5f253857669da9610ee1771d" + }, + { + "name": "CSS Animation", + "slug": "css-animation", + "objectID": "567c03e03f1768f6bf48a678" + }, + { + "name": "books", + "slug": "books", + "objectID": "56744721958ef13879b94d2a" + }, + { + "name": "Technical interview", + "slug": "technical-interview", + "objectID": "5f0725a8570e2e29ce255012" + }, + { + "name": "PHP7", + "slug": "php7", + "objectID": "5680fde5aeae5c9e229cf8e2" + }, + { + "name": "side project", + "slug": "side-project", + "objectID": "576fa8aca245bcf2e2e91044" + }, + { + "name": "personal", + "slug": "personal", + "objectID": "56b41c593f1e4ff03c56b4e4" + }, + { + "name": "github-actions", + "slug": "github-actions-1", + "objectID": "5f4f0f5850b5c61ec6ef4eb4" + }, + { + "name": "Facebook", + "slug": "facebook", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496176300/khcjk48ycpejt19sfgav.png", + "objectID": "56744721958ef13879b94da0" + }, + { + "name": "code review", + "slug": "code-review", + "objectID": "56744721958ef13879b949f9" + }, + { + "name": "elasticsearch", + "slug": "elasticsearch", + "objectID": "56744723958ef13879b95430" + }, + { + "name": "TDD (Test-driven development)", + "slug": "tdd", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502441836/zcgicxrtkoquz67dlkgs.png", + "objectID": "56744721958ef13879b94898" + }, + { + "name": "Svelte", + "slug": "svelte", + "objectID": "583d0951f533d193a2e694d1" + }, + { + "name": "Sass", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1490082271/p0drxprfhz9qmkm0txrf.png", + "slug": "sass", + "objectID": "56744721958ef13879b94df7" + }, + { + "name": "Entrepreneurship", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496913550/abhdc0juuwrn1kfjk966.png", + "slug": "entrepreneurship", + "objectID": "567a50052b926c3063c305c9" + }, + { + "name": "Bugs and Errors", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1465891233/e2dpkrprraf8jitcvc3i.png", + "slug": "bugs-and-errors", + "objectID": "575f9bc3da600b8ef43e5263" + }, + { + "name": "android apps", + "slug": "android-apps", + "objectID": "590c655dd7c4344afe6c3241" + }, + { + "name": "Flutter Widgets", + "slug": "flutter-widgets", + "objectID": "5f08f6a1b0bf5b3c273ea78c" + }, + { + "name": "documentation", + "slug": "documentation", + "objectID": "56744722958ef13879b950f8" + }, + { + "name": "Continuous Integration", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1460096831/jeyz4slnhjuflhkqbanb.png", + "slug": "continuous-integration", + "objectID": "56744721958ef13879b94de0" + }, + { + "name": "version control", + "slug": "version-control", + "objectID": "56744722958ef13879b9506b" + }, + { + "name": "asynchronous", + "slug": "asynchronous", + "objectID": "56744722958ef13879b94e66" + }, + { + "name": "Magento", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1487144361/a8xaya1bv8advcuoj90m.png", + "slug": "magento", + "objectID": "56eadc94bcca2d711e191c4c" + }, + { + "name": "Netlify", + "slug": "netlify", + "objectID": "57ce27e495368c463b098050" + }, + { + "name": "nginx", + "slug": "nginx", + "objectID": "56744722958ef13879b94f8b" + }, + { + "name": "web scraping", + "slug": "web-scraping", + "objectID": "58dfb250eb0ffea9e764936d" + }, + { + "name": "ios app development", + "slug": "ios-app-development", + "objectID": "584a50f7e1ffd7084c8b1e6c" + }, + { + "name": "Redis", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513324425585/r1M9y-bMM.png", + "slug": "redis", + "objectID": "56744721958ef13879b94c41" + }, + { + "name": "infrastructure", + "slug": "infrastructure", + "objectID": "56a4e1d28e1dd6d05014efdb" + }, + { + "name": "shell", + "slug": "shell", + "objectID": "56744723958ef13879b95561" + }, + { + "name": "CSS Frameworks", + "slug": "css-frameworks", + "objectID": "56744721958ef13879b94b82" + }, + { + "name": "Responsive Web Design", + "slug": "responsive-web-design", + "objectID": "574dc610be8cff2ed6571a40" + }, + { + "name": "bootcamp", + "slug": "bootcamp", + "objectID": "58d54af36047f98ddcae780b" + }, + { + "name": "Competitive programming", + "slug": "competitive-programming", + "objectID": "56fb79d4da7018d48c208e91" + }, + { + "name": "podcast", + "slug": "podcast", + "objectID": "56744722958ef13879b950d3" + }, + { + "name": "email", + "slug": "email", + "objectID": "56744722958ef13879b95038" + }, + { + "name": "Material Design", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1474050455/stm5thxo0n1evvzpl7np.png", + "slug": "material-design", + "objectID": "56744722958ef13879b95029" + }, + { + "name": "NoSQL", + "slug": "nosql", + "objectID": "56744721958ef13879b94b41" + }, + { + "name": "markdown", + "slug": "markdown", + "objectID": "56744722958ef13879b950b2" + }, + { + "name": "components", + "slug": "components", + "objectID": "571c5374fc5b53a1ace37ce8" + }, + { + "name": "unit testing", + "slug": "unit-testing", + "objectID": "56744721958ef13879b94ac4" + }, + { + "name": "management", + "slug": "management", + "objectID": "56744721958ef13879b948d1" + }, + { + "name": "research", + "slug": "research", + "objectID": "56744723958ef13879b952cb" + }, + { + "name": "Ionic Framework", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1513839196650/HyrD9A_fM.jpeg", + "slug": "ionic", + "objectID": "56744721958ef13879b94b62" + }, + { + "name": "vim", + "slug": "vim", + "objectID": "56744722958ef13879b95126" + }, + { + "name": "Accessibility", + "slug": "accessibility", + "objectID": "56744723958ef13879b95230" + }, + { + "name": "remote", + "slug": "remote", + "objectID": "56744721958ef13879b94841" + }, + { + "name": "agile", + "slug": "agile", + "objectID": "56744723958ef13879b9551b" + }, + { + "name": "analytics", + "slug": "analytics", + "objectID": "56744721958ef13879b9495b" + }, + { + "name": "vscode extensions", + "slug": "vscode-extensions", + "objectID": "5f5c6d4213599a5f2e33f00f" + }, + { + "name": "statistics", + "slug": "statistics", + "objectID": "56744721958ef13879b949ea" + }, + { + "name": "react router", + "slug": "react-router", + "objectID": "56744721958ef13879b949bc" + }, + { + "name": "IDEs", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468381/x5vcqb3xxe7wdheuopww.png", + "slug": "ides", + "objectID": "56744722958ef13879b94eff" + }, + { + "name": "forms", + "slug": "forms", + "objectID": "56744721958ef13879b948fa" + }, + { + "name": "Terraform", + "slug": "terraform", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1617121672721/6r0bN-GSK.png", + "objectID": "57bf546693309a25047c6206" + }, + { + "name": "animation", + "slug": "animation", + "objectID": "56744723958ef13879b95338" + }, + { + "name": "Developer Blogging", + "slug": "developer-blogging", + "objectID": "5f1c1e25e8769101a9ef64d2" + }, + { + "name": "PWA", + "slug": "pwa", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496404433/rlgbcgsuycivf0ukgxrl.png", + "objectID": "57cbc5d49b3eb82e014a0320" + }, + { + "name": "JAMstack", + "slug": "jamstack", + "objectID": "58f9253e01cb858c63429c31" + }, + { + "name": "Elixir", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1452000435/svfntjev0f681f6oiptm.png", + "slug": "elixir", + "objectID": "56744723958ef13879b95392" + }, + { + "name": "dotnetcore", + "slug": "dotnetcore", + "objectID": "5794f65abecb9ebac0d5fc56" + }, + { + "name": "ShowHashnode", + "slug": "showhashnode", + "objectID": "5d946e601971c92f3298b280" + }, + { + "name": "coding challenge", + "slug": "coding-challenge", + "objectID": "5f16831dfefe35614464e44b" + }, + { + "name": "Android Studio", + "slug": "android-studio", + "objectID": "5868042db99398bc30c43e77" + }, + { + "name": "variables", + "slug": "variables", + "objectID": "56744721958ef13879b94863" + }, + { + "name": "ci-cd", + "slug": "ci-cd", + "objectID": "5f0ed0dd7611e111fbd7194f" + }, + { + "name": "nlp", + "slug": "nlp", + "objectID": "573a8e38a5dc678fc9090d31" + }, + { + "name": "#howtos", + "slug": "howtos", + "objectID": "5f18178960b5d372e20d5a86" + }, + { + "name": "Web Hosting", + "slug": "web-hosting", + "objectID": "571faab486b33947d9bdbab2" + }, + { + "name": "oop", + "slug": "oop", + "objectID": "5674471d958ef13879b94779" + }, + { + "name": "DigitalOcean", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1491567594/mtbp3w2posceqcdj8rx5.jpg", + "slug": "digitalocean", + "objectID": "56744721958ef13879b948c3" + }, + { + "name": "SVG", + "slug": "svg", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1500325020/kp4vjytdfuqbhaibiqm7.png", + "objectID": "56744723958ef13879b95469" + }, + { + "name": "promises", + "slug": "promises", + "objectID": "56744722958ef13879b951d9" + }, + { + "name": "womenwhocode", + "slug": "womenwhocode", + "objectID": "5f1fdd28ed20ff21a11e7126" + }, + { + "name": "Flutter SDK", + "slug": "flutter-sdk", + "objectID": "5f08f6a1b0bf5b3c273ea78a" + }, + { + "name": "optimization", + "slug": "optimization", + "objectID": "56744721958ef13879b94821" + }, + { + "name": "work", + "slug": "work", + "objectID": "56a361abff99ae055eeffd33" + }, + { + "name": "database", + "slug": "database", + "objectID": "56744722958ef13879b950ef" + }, + { + "name": "pandas", + "slug": "pandas", + "objectID": "56744723958ef13879b953e6" + }, + { + "name": "chrome extension", + "slug": "chrome-extension", + "objectID": "56b1945b04f0061506b361db" + }, + { + "name": "privacy", + "slug": "privacy", + "objectID": "56744723958ef13879b952fc" + }, + { + "name": "events", + "slug": "events", + "objectID": "575d75e2da600b8ef43e506d" + }, + { + "name": "ansible", + "slug": "ansible", + "objectID": "56744722958ef13879b95152" + }, + { + "name": "Mathematics", + "slug": "mathematics", + "objectID": "592d60cb8a6f7b0a1195412a" + }, + { + "name": "startup", + "slug": "startup", + "objectID": "56744721958ef13879b94bbb" + }, + { + "name": "music", + "slug": "music", + "objectID": "56744721958ef13879b949c6" + }, + { + "name": "problem solving skills", + "slug": "problem-solving-skills", + "objectID": "5f8560a8e83ccb407537a1ee" + }, + { + "name": "review", + "slug": "review", + "objectID": "56744723958ef13879b953b4" + }, + { + "name": "GIS", + "slug": "gis", + "objectID": "57fb4f226849a80ac266ca71" + }, + { + "name": "unity", + "slug": "unity", + "objectID": "56744721958ef13879b94885" + }, + { + "name": "test", + "slug": "test", + "objectID": "56744722958ef13879b951d6" + }, + { + "name": "TIL", + "slug": "til", + "objectID": "5d93238ce235795f6eb6dd79" + }, + { + "name": "Auth0", + "slug": "auth0", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1627916134204/sEaEU0wiP.png", + "objectID": "56fb1506ea33a5b266f2ffc3" + }, + { + "name": "Certification", + "slug": "certification", + "objectID": "57d4461ed17cab545cab66de" + }, + { + "name": "webdevelopment", + "slug": "webdevelopment", + "objectID": "56744721958ef13879b94b20" + }, + { + "name": "lifestyle", + "slug": "lifestyle", + "objectID": "56744721958ef13879b948f2" + }, + { + "name": "course", + "slug": "course", + "objectID": "575150c412a8cb07bb842118" + }, + { + "name": "Story", + "slug": "story", + "objectID": "57348ce934963cba3535abb4" + }, + { + "name": "job search", + "slug": "job-search", + "objectID": "5f08ee681981c53c4987f2b4" + }, + { + "name": "Raspberry Pi", + "slug": "raspberry-pi", + "objectID": "56d2cbb4099859fa044d68c0" + }, + { + "name": "Amazon Web Services", + "slug": "amazon-web-services", + "objectID": "56a6742dc84f2c6913b8eac3" + }, + { + "name": "tutorials", + "slug": "tutorials", + "objectID": "56744721958ef13879b94dcc" + }, + { + "slug": "flutter-cjx3aa7op001jims1kuwl3ekz", + "objectID": "5d0a3b36c7de780e772aff0a" + }, + { + "name": "#data visualisation", + "slug": "data-visualisation-1", + "objectID": "5f4b7d61f540845bb26f0291" + }, + { + "name": "continuous deployment", + "slug": "continuous-deployment", + "objectID": "56744722958ef13879b94f92" + }, + { + "name": "video", + "slug": "video", + "objectID": "56744723958ef13879b954e9" + }, + { + "name": "DOM", + "slug": "dom", + "objectID": "56744723958ef13879b95376" + }, + { + "name": "search", + "slug": "search", + "objectID": "56744721958ef13879b9497b" + }, + { + "name": "JWT", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1464240237/bqu9k0lklrg7xxvk2pzq.jpg", + "slug": "jwt", + "objectID": "56744723958ef13879b9536e" + }, + { + "name": "Interviews", + "slug": "interviews", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496318621/g6yz7ukrqftqat2y3ycn.png", + "objectID": "56744721958ef13879b948b1" + }, + { + "name": "vanilla-js", + "slug": "vanilla-js-1", + "objectID": "5f9aaeaba1658252d1a7b620" + }, + { + "name": "monitoring", + "slug": "monitoring", + "objectID": "56744723958ef13879b95361" + }, + { + "name": "Text Editors", + "slug": "text-editors", + "objectID": "571459a7162bdaad9f92b0d7" + }, + { + "name": "gaming", + "slug": "gaming", + "objectID": "57e951b155544e5132a4d5df" + }, + { + "name": "mongoose", + "slug": "mongoose", + "objectID": "56744723958ef13879b9540c" + }, + { + "name": "SaaS", + "slug": "saas", + "objectID": "56744722958ef13879b950a5" + }, + { + "name": "content", + "slug": "content", + "objectID": "56744721958ef13879b94849" + }, + { + "name": "apache", + "slug": "apache", + "objectID": "56744723958ef13879b95513" + }, + { + "name": "engineering", + "slug": "engineering", + "objectID": "56744722958ef13879b950b5" + }, + { + "name": "headless cms", + "slug": "headless-cms", + "objectID": "5914be36db93b4aae8008897" + }, + { + "name": "newsletter", + "slug": "newsletter", + "objectID": "56744722958ef13879b9516a" + }, + { + "name": "network", + "slug": "network", + "objectID": "56744721958ef13879b94923" + }, + { + "name": "IT", + "slug": "it", + "objectID": "57628dcd820dd45f3fbd8eb5" + }, + { + "name": "mobile app development", + "slug": "mobile-app-development", + "objectID": "56744723958ef13879b95222" + }, + { + "name": "freeCodeCamp.org", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1518534240940/ByFDRugwf.jpeg", + "slug": "freecodecamp", + "objectID": "57039f98f950faa9ab7ec552" + }, + { + "name": "Cryptography", + "slug": "cryptography", + "objectID": "58426a8997063da359fe2cf4" + }, + { + "name": "Augmented Reality", + "slug": "augmented-reality", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1506666999/lnnrwwh9td4xm87lh13d.png", + "objectID": "57ce29fde5e41a2a5c24fa98" + }, + { + "name": "training", + "slug": "training", + "objectID": "56b0a1600a7ca0c6f70c3703" + }, + { + "name": "Objects", + "slug": "objects", + "objectID": "57e793cdef99cf03582fe42b" + }, + { + "name": "flexbox", + "slug": "flexbox", + "objectID": "56744721958ef13879b94afb" + }, + { + "name": "SSL", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450712342/u2tfvtrfojyne6qzaflq.jpg", + "slug": "ssl", + "objectID": "56744721958ef13879b94912" + }, + { + "name": "ASP.NET", + "slug": "aspnet", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1515075607732/ByxXu3jQG.jpeg", + "objectID": "567e2a600db88211bac0a032" + }, + { + "name": "distributed system", + "slug": "distributed-system", + "objectID": "568c90725e7a940b3d3e08ed" + }, + { + "name": "logging", + "slug": "logging", + "objectID": "568bb9dbe99c5444f3233893" + }, + { + "name": "Applications", + "slug": "applications", + "objectID": "56ea7aebbcca2d711e191c02" + }, + { + "name": "user experience", + "slug": "user-experience", + "objectID": "56744721958ef13879b948d4" + }, + { + "name": "architecture", + "slug": "architecture", + "objectID": "56744723958ef13879b9529a" + }, + { + "name": "package", + "slug": "package", + "objectID": "56744723958ef13879b9533c" + }, + { + "name": "tricks", + "slug": "tricks", + "objectID": "56744721958ef13879b94b19" + }, + { + "name": "R Language", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1490864688/fiw7ngemmxkumjpkntdp.png", + "slug": "r", + "objectID": "56744722958ef13879b95111" + }, + { + "name": "css flexbox", + "slug": "css-flexbox", + "objectID": "56744721958ef13879b94c3a" + }, + { + "name": "Xcode", + "slug": "xcode", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502355718/vddmkshskl3sbl4xogwn.jpg", + "objectID": "56744720958ef13879b947ff" + }, + { + "name": "Monetization", + "slug": "monetization", + "objectID": "5736a1db6a4640415dc89e28" + }, + { + "name": "async", + "slug": "async", + "objectID": "56cbdb23b70682283f9edeb8" + }, + { + "name": "SQL Server", + "slug": "sql-server", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1551380133166/kHlXAcxdU.jpeg", + "objectID": "56744720958ef13879b947b6" + }, + { + "name": "tensorflow", + "slug": "tensorflow", + "objectID": "56744722958ef13879b9518a" + }, + { + "name": "Vercel Hashnode Hackathon", + "slug": "vercelhashnode", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1610701406772/nrAD-f_i6.png", + "objectID": "6001530cf611a365208ad66a" + }, + { + "name": "extension", + "slug": "extension", + "objectID": "569f6b4492921b8f79d36061" + }, + { + "name": "free", + "slug": "free", + "objectID": "56744723958ef13879b95214" + }, + { + "name": "kotlin beginner", + "slug": "kotlin-beginner", + "objectID": "5f081e73b587713318b74a42" + }, + { + "name": "SurviveJS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1461251276/bmmcz554bnl0zk83l1iz.png", + "slug": "survivejs", + "objectID": "5718ec0fc4b104334fad928e" + }, + { + "name": "Rails", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1453793832/qypx8pjpm7tybpcbfhif.jpg", + "slug": "rails", + "objectID": "56744722958ef13879b94eb5" + }, + { + "name": "Web Perf", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472485302/gvihfpia52e0l5r3rau9.jpg", + "slug": "webperf", + "objectID": "56744722958ef13879b950c6" + }, + { + "name": "big data", + "slug": "big-data", + "objectID": "56744721958ef13879b94e3b" + }, + { + "name": "communication", + "slug": "communication", + "objectID": "57d2d92415ae0c65b80ace44" + }, + { + "name": "Solidity", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1512988916861/ryTxWknbG.png", + "slug": "solidity", + "objectID": "595ab8b5a3e02ebe146b2f2a" + }, + { + "name": "Experience ", + "slug": "experience", + "objectID": "587dbc32d40f782e50cf92e0" + }, + { + "name": "Amazon S3", + "slug": "amazon-s3", + "objectID": "569d145446dfdb8479aa690d" + }, + { + "name": "Meteor", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450467991/aaskzxstfaadd1sbhxj2.png", + "slug": "meteor", + "objectID": "56744722958ef13879b94fa7" + }, + { + "name": "agile development", + "slug": "agile-development", + "objectID": "56744721958ef13879b94dba" + }, + { + "name": "Oracle", + "slug": "oracle", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516182996908/ByaA6q2NG.jpeg", + "objectID": "56744721958ef13879b9498a" + }, + { + "name": "scss", + "slug": "scss", + "objectID": "56744722958ef13879b951f1" + }, + { + "name": "GCP", + "slug": "gcp", + "objectID": "58d4d1fbcfc5bd6596a0a6b5" + }, + { + "name": "domain", + "slug": "domain", + "objectID": "5714fe4e151fa7c4488cc1ae" + }, + { + "name": "Regex", + "slug": "regex", + "objectID": "56f6aef0aa013a5f87413615" + }, + { + "name": "Symfony", + "slug": "symfony", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1541163459741/BknTF6t3m.png", + "objectID": "572d6c67bf97af427dd07f13" + }, + { + "name": "app", + "slug": "app", + "objectID": "56744721958ef13879b94a0e" + }, + { + "name": "Junior developer ", + "slug": "junior-developer", + "objectID": "5f071caa6e04d8269a566170" + }, + { + "name": "advice", + "slug": "advice", + "objectID": "56744723958ef13879b95333" + }, + { + "name": "Powershell", + "slug": "powershell", + "objectID": "56f7871ffc7154468758edb7" + }, + { + "name": "Babel", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1504815622/wo9hjfe0klgxj8mahf6j.png", + "slug": "babel", + "objectID": "56744722958ef13879b95045" + }, + { + "name": "Reactive Programming", + "slug": "reactive-programming", + "objectID": "56744721958ef13879b94aee" + }, + { + "name": "Smart Contracts", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1512989048807/S1WtW1hZz.png", + "slug": "smart-contracts", + "objectID": "5a2e407a5b9ed1636662b8f9" + }, + { + "name": "string", + "slug": "string", + "objectID": "57448e2a9ade925885158cfe" + }, + { + "name": "images", + "slug": "images", + "objectID": "56744723958ef13879b95229" + }, + { + "name": "hiring", + "slug": "hiring", + "objectID": "56744721958ef13879b9497e" + }, + { + "name": "Christmas Hackathon", + "slug": "christmashackathon", + "logo": null, + "objectID": "5fe187955620145ec6e3a5c2" + }, + { + "name": "services", + "slug": "services", + "objectID": "5682e64e2c29f7e0c86d024b" + }, + { + "name": "aws-cdk", + "slug": "aws-cdk", + "objectID": "5f743910a3a6d515f7142eb4" + }, + { + "name": "Laravel 5", + "slug": "laravel-5", + "objectID": "56ec06ac5edec9d7189a0ad6" + }, + { + "name": "crypto", + "slug": "crypto", + "objectID": "57b188c971be21426cb4916e" + }, + { + "name": "instagram", + "slug": "instagram", + "objectID": "56744721958ef13879b94aec" + }, + { + "name": "questions", + "slug": "questions", + "objectID": "56744723958ef13879b952fe" + }, + { + "name": "bot", + "slug": "bot", + "objectID": "56744721958ef13879b948df" + }, + { + "name": "chatbot", + "slug": "chatbot", + "objectID": "57444f35468ae9e479434fac" + }, + { + "name": "risingstack", + "slug": "risingstack", + "objectID": "587745676b985e96ec6d48b7" + }, + { + "name": "trends", + "slug": "trends", + "objectID": "56744721958ef13879b94a2a" + }, + { + "name": "Jest", + "slug": "jest", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496389933/s2t8atgotu6wvjgojpn6.png", + "objectID": "56cfe81bfa28f5fe7f74d215" + }, + { + "name": "refactoring", + "slug": "refactoring", + "objectID": "56744720958ef13879b947df" + }, + { + "name": "frameworks", + "slug": "frameworks", + "objectID": "56744721958ef13879b94db1" + }, + { + "name": "arrays", + "slug": "arrays", + "objectID": "579350e1d87e23e5efe30d84" + }, + { + "name": "cheatsheet", + "slug": "cheatsheet", + "objectID": "56cc66fff978c91273a36237" + }, + { + "name": "team", + "slug": "team", + "objectID": "56744723958ef13879b952e7" + }, + { + "name": "docker images", + "slug": "docker-images", + "objectID": "5f442ff51b2ea309b7529267" + }, + { + "name": "classes", + "slug": "classes", + "objectID": "56744723958ef13879b955a3" + }, + { + "name": "workflow", + "slug": "workflow", + "objectID": "56744722958ef13879b94e77" + }, + { + "name": "ML", + "slug": "ml", + "objectID": "57c6e7bdb274bac7e601abe2" + }, + { + "name": "neural networks", + "slug": "neural-networks", + "objectID": "56af3b4ccc975f0cc6878c8a" + }, + { + "name": "javascript modules", + "slug": "javascript-modules", + "objectID": "56cbdab9b70682283f9edeae" + }, + { + "name": "skills", + "slug": "skills", + "objectID": "576b3918decdd3bf3610c80b" + }, + { + "name": "Internet of Things", + "slug": "internet-of-things", + "objectID": "58f8acb0e928dad5e4c7ab2b" + }, + { + "name": "dns", + "slug": "dns", + "objectID": "5674471d958ef13879b94798" + }, + { + "name": "Blazor ", + "slug": "blazor-1", + "objectID": "5f219f52ef20f63bcf9822c6" + }, + { + "name": "Script", + "slug": "script", + "objectID": "56a294beff99ae055eeffcea" + }, + { + "name": "Help Needed", + "slug": "help", + "objectID": "5674471d958ef13879b94764" + }, + { + "name": "mobile", + "slug": "mobile", + "objectID": "56744723958ef13879b9524e" + }, + { + "name": "Amplify Hashnode", + "slug": "amplifyhashnode", + "logo": null, + "objectID": "60223d4f281265375d643d83" + }, + { + "name": "ssh", + "slug": "ssh", + "objectID": "5677ff6aec7aa67e51f1e096" + }, + { + "name": "Software Testing", + "slug": "software-testing", + "objectID": "56b54dae8dabdc6142c1ac86" + }, + { + "name": "dev tools", + "slug": "dev-tools", + "objectID": "56744723958ef13879b9527c" + }, + { + "name": "https", + "slug": "https", + "objectID": "56744722958ef13879b94e73" + }, + { + "name": "Inspiration", + "slug": "inspiration", + "objectID": "57de56e3c61e5b59729da2a8" + }, + { + "name": "Ajax", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1459504130/huzynvc2g3hd5w8sjw6w.jpg", + "slug": "ajax", + "objectID": "56744722958ef13879b95140" + }, + { + "name": "DEVCommunity", + "slug": "devcommunity", + "objectID": "5f1ccb30f4016901885cc50f" + }, + { + "name": "oauth", + "slug": "oauth", + "objectID": "56744722958ef13879b951b1" + }, + { + "name": "design principles", + "slug": "design-principles", + "objectID": "5f965c1c40346172a86c2c4b" + }, + { + "name": "mentalhealth", + "slug": "mentalhealth-1", + "objectID": "5f7e39240e5d207780d949e9" + }, + { + "name": "#hacktoberfest ", + "slug": "hacktoberfest-1", + "objectID": "5f6629266dfc523d0a89357b" + }, + { + "name": "MobX", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1534512814483/SJUnRS4U7.jpeg", + "slug": "mobx", + "objectID": "5729bc14faa06f875ef32e95" + }, + { + "name": "ec2", + "slug": "ec2", + "objectID": "56744721958ef13879b94a18" + }, + { + "name": "setup", + "slug": "setup", + "objectID": "57a37bf75bfdd08aeffb5832" + }, + { + "name": "devtools", + "slug": "devtools", + "objectID": "56744722958ef13879b950fe" + }, + { + "name": "ecmascript", + "slug": "ecmascript", + "objectID": "56744722958ef13879b9511f" + }, + { + "name": "styled-components", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1486104606/jbhiqodxlyhaqogfuqwy.png", + "slug": "styled-components", + "objectID": "58900d47afa2b4bce2efb44f" + }, + { + "name": "REST", + "slug": "rest", + "objectID": "56744721958ef13879b949f6" + }, + { + "name": "caching", + "slug": "caching", + "objectID": "56744723958ef13879b9540f" + }, + { + "name": "7daystreak", + "slug": "7daystreak", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1626280769878/xaxdZgS0N.png", + "objectID": "60ed9e18fc37a15ec15683b3" + }, + { + "name": "image processing", + "slug": "image-processing", + "objectID": "5674471d958ef13879b94776" + }, + { + "name": "Web API", + "slug": "web-api", + "objectID": "5894ec2f47e4163deb72c252" + }, + { + "name": "ideas", + "slug": "ideas", + "objectID": "56744721958ef13879b948f6" + }, + { + "name": "hack", + "slug": "hack", + "objectID": "56744723958ef13879b95426" + }, + { + "name": "hardware", + "slug": "hardware", + "objectID": "568439646b179c61d167f08d" + }, + { + "name": "web application", + "slug": "web-application", + "objectID": "56744723958ef13879b952c2" + }, + { + "name": "library", + "slug": "library", + "objectID": "56744721958ef13879b94d94" + }, + { + "name": "opencv", + "slug": "opencv", + "objectID": "587745676b985e96ec6d48b8" + }, + { + "name": "AWS Certified Solutions Architect Associate", + "slug": "aws-certified-solutions-architect-associate", + "objectID": "5f71b762eb14b172f1d4bc39" + }, + { + "name": "CSS Grid", + "slug": "css-grid", + "objectID": "58becf402a99d222c65c24d8" + }, + { + "name": "job", + "slug": "job", + "objectID": "56744721958ef13879b94a46" + }, + { + "name": "leadership", + "slug": "leadership", + "objectID": "57c15e52387df20e0b9f94a0" + }, + { + "name": "Jenkins", + "slug": "jenkins", + "objectID": "57d6d71cf72dd3705c15ffcf" + }, + { + "name": "eslint", + "slug": "eslint", + "objectID": "570f716a115103c3b0978698" + }, + { + "name": "time", + "slug": "time", + "objectID": "58f7bab0e1eb1bd4e45f05f0" + }, + { + "name": "realtime", + "slug": "realtime", + "objectID": "56744721958ef13879b94bdf" + }, + { + "name": "Math", + "slug": "math", + "objectID": "581ad086c055bbfb46d8811b" + }, + { + "name": "conference", + "slug": "conference", + "objectID": "56744721958ef13879b9493b" + }, + { + "name": "general", + "slug": "general", + "objectID": "56fd6444404be5549d3de51b" + }, + { + "name": "encryption", + "slug": "encryption", + "objectID": "56744723958ef13879b9528d" + }, + { + "name": "files", + "slug": "files", + "objectID": "57f7bbb9813841efc19c3488" + }, + { + "name": "error handling", + "slug": "error-handling", + "objectID": "56744722958ef13879b95084" + }, + { + "name": "Auth0Hackathon", + "slug": "auth0hackathon", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1627916253311/DuFTo1seC.png", + "objectID": "6108059fb97c436d241bddc5" + }, + { + "name": "numpy", + "slug": "numpy", + "objectID": "57c7c7c7e53060955aa8c018" + }, + { + "name": "D3.js", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1459873316/fdlqr3pk587gddsrirxe.jpg", + "slug": "d3js", + "objectID": "56744721958ef13879b94d8c" + }, + { + "name": "Apollo GraphQL", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1467922175/sbxeze75uotah3qeqhbh.png", + "slug": "apollo", + "objectID": "57053ef1115103c3b0977fb0" + }, + { + "name": "Nuxt", + "slug": "nuxt", + "objectID": "591c5a1956856e7d71046403" + }, + { + "name": "DDD", + "slug": "ddd", + "objectID": "576b14ad41d2cbca360cf875" + }, + { + "name": "excel", + "slug": "excel", + "objectID": "591414b39e2b75ff7c5fa62d" + }, + { + "name": "branding", + "slug": "branding", + "objectID": "56b71ac92894c38346c06670" + }, + { + "name": "Web Components", + "slug": "web-components", + "objectID": "56744723958ef13879b95564" + }, + { + "name": "dynamodb", + "slug": "dynamodb", + "objectID": "56744722958ef13879b950d8" + }, + { + "name": "College", + "slug": "college", + "objectID": "587dbc32d40f782e50cf92df" + }, + { + "name": "journal", + "slug": "journal", + "objectID": "5674471d958ef13879b94791" + }, + { + "name": "state", + "slug": "state", + "objectID": "584ac47b9747b36ae2a28c8a" + }, + { + "name": "impostor syndrome", + "slug": "impostor-syndrome", + "objectID": "56744723958ef13879b95306" + }, + { + "name": "creativity", + "slug": "creativity", + "objectID": "56744721958ef13879b94829" + }, + { + "name": "SheCodeAfrica ", + "slug": "shecodeafrica", + "objectID": "5f115a51d6c58d29e0240e45" + }, + { + "name": "SocketIO", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472485355/zsypm63fq6998mc1pqvl.png", + "slug": "socketio", + "objectID": "56744721958ef13879b94b52" + }, + { + "name": "HTML Canvas", + "slug": "html-canvas", + "objectID": "5692580fcad8946e563c570a" + }, + { + "name": "QA", + "slug": "qa", + "objectID": "56a20c4d92921b8f79d36276" + }, + { + "name": "linux kernel", + "slug": "linux-kernel", + "objectID": "5faadc16d6009557c49f5bbb" + }, + { + "name": "Travel", + "slug": "travel", + "objectID": "58859588abf4ad10c6ac08b6" + }, + { + "name": "authorization", + "slug": "authorization", + "objectID": "56744722958ef13879b9518c" + }, + { + "name": "Scrum", + "slug": "scrum", + "objectID": "570a9a273aeb5317437380e4" + }, + { + "name": "Validation", + "slug": "validation", + "objectID": "56c093923ddee41359169468" + }, + { + "name": "messaging", + "slug": "messaging", + "objectID": "57d832bbd17cab545cab9dbf" + }, + { + "name": "Computer Vision", + "slug": "computer-vision", + "objectID": "57534dab82cbbab8dcd475b9" + }, + { + "name": "ios app developer", + "slug": "ios-app-developer", + "objectID": "56744723958ef13879b9542c" + }, + { + "name": "Xamarin", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1464701189/ms3lwj8fdp2agynrxrly.jpg", + "slug": "xamarin", + "objectID": "56744721958ef13879b94825" + }, + { + "name": "mvc", + "slug": "mvc", + "objectID": "56744721958ef13879b94995" + }, + { + "name": "fonts", + "slug": "fonts", + "objectID": "56744721958ef13879b9499e" + }, + { + "name": "video streaming", + "slug": "video-streaming", + "objectID": "590c71fe1ae3d06072e8956c" + }, + { + "name": "closure", + "slug": "closure", + "objectID": "56744721958ef13879b94b1e" + }, + { + "name": "HarperDB Hackathon", + "slug": "harperdbhackathon", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1623401171709/jCXKCcOIl.png", + "objectID": "60b7952425dc276ffb940618" + }, + { + "name": "axios", + "slug": "axios", + "objectID": "58887bba81421379798066f5" + }, + { + "name": "mentorship", + "slug": "mentorship", + "objectID": "575c2d212f07b512c4dce579" + }, + { + "name": "code smell ", + "slug": "code-smell-1", + "objectID": "5fa7f4cac0d56c5ae62e3471" + }, + { + "name": "Web Accessibility", + "slug": "web-accessibility", + "objectID": "5f3f1dcc5b3ac8481821c47c" + }, + { + "name": "#growth", + "slug": "growth-1", + "objectID": "5f21ee72ef20f63bcf98250b" + }, + { + "name": "shopify", + "slug": "shopify", + "objectID": "57d2f8b8739df23de32d9a0b" + }, + { + "name": "dailydev", + "slug": "dailydev", + "objectID": "5f4e6e6de613341d6f8cd33e" + }, + { + "name": "expressjs", + "slug": "expressjs", + "objectID": "56744721958ef13879b94d81" + }, + { + "name": "fun", + "slug": "fun", + "objectID": "56744723958ef13879b954b1" + }, + { + "name": "android development", + "slug": "android-development", + "objectID": "56744722958ef13879b95086" + }, + { + "name": "DevBlogging", + "slug": "devblogging", + "objectID": "5f323f334332ee07eb55c25e" + }, + { + "name": "Scala", + "slug": "scala", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496318498/u1ogtyiakscd683ar63g.png", + "objectID": "56744723958ef13879b952a7" + }, + { + "name": "repository", + "slug": "repository", + "objectID": "56744721958ef13879b94932" + }, + { + "name": "Gulp", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1455107024/ymnvwdrzghdaupgnh1pa.png", + "slug": "gulp", + "objectID": "56744723958ef13879b954b9" + }, + { + "name": "CodePen", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1490300926/zpeedkxkcyorvxzepwdq.png", + "slug": "codepen", + "objectID": "56744722958ef13879b94f3e" + }, + { + "name": "front-end", + "slug": "front-end-cik5w32oi016zos53hitiymhh", + "objectID": "56b118e610979efc2b9a8d91" + }, + { + "name": "Salesforce", + "slug": "salesforce", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1542629160319/SkxND7eC7.jpeg", + "objectID": "578d40c45460288cdeb6f094" + }, + { + "name": "Auth ", + "slug": "auth", + "objectID": "5762d998d163d06a3fca2d8d" + }, + { + "name": "sorting", + "slug": "sorting", + "objectID": "56e79a12c10bbcfb0ce541b1" + }, + { + "name": "slack", + "slug": "slack", + "objectID": "56744723958ef13879b952bc" + }, + { + "name": "languages", + "slug": "languages", + "objectID": "56744723958ef13879b95347" + }, + { + "name": "Amazon", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1469216724/nxiuwpm6dybqbn9dhybc.png", + "slug": "amazon", + "objectID": "56744721958ef13879b94906" + }, + { + "name": "storage", + "slug": "storage", + "objectID": "5708ff9c115103c3b09782d7" + }, + { + "name": "algorithm", + "slug": "algorithm", + "objectID": "56744721958ef13879b94de3" + }, + { + "name": "pdf", + "slug": "pdf", + "objectID": "57962622bdb2f5db657ae6c3" + }, + { + "name": "fetch", + "slug": "fetch", + "objectID": "5758618112a8cb07bb8426d2" + }, + { + "name": "dependency injection", + "slug": "dependency-injection", + "objectID": "56e6d5598c0bb8288a559c95" + }, + { + "name": "template", + "slug": "template", + "objectID": "56c4cd6eedfec14f66f81d98" + }, + { + "name": "RxJS", + "slug": "rxjs", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1512113179321/HJXmNY0lf.jpeg", + "objectID": "56744723958ef13879b95559" + }, + { + "name": "WebAssembly", + "slug": "webassembly", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1510296821/n90fqabiufcs8kbridxm.png", + "objectID": "56744722958ef13879b95043" + }, + { + "name": "game", + "slug": "game", + "objectID": "56744721958ef13879b9496d" + }, + { + "name": "lambda", + "slug": "lambda", + "objectID": "56744721958ef13879b94867" + }, + { + "name": "JSX", + "slug": "jsx", + "objectID": "577b65e0a1ac2f52aea75814" + }, + { + "name": "GUI", + "slug": "gui", + "objectID": "574dd005be8cff2ed6571a4f" + }, + { + "name": "theme", + "slug": "theme", + "objectID": "58e1a2b84200d85d6bfc1457" + }, + { + "name": "routing", + "slug": "routing", + "objectID": "56744721958ef13879b949fb" + }, + { + "name": "Firefox", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1511214305890/HJ9ka6gxM.jpeg", + "slug": "firefox", + "objectID": "56744721958ef13879b94929" + }, + { + "name": "visual studio", + "slug": "visual-studio", + "objectID": "56744723958ef13879b953df" + }, + { + "name": "migration", + "slug": "migration", + "objectID": "56744723958ef13879b9534f" + }, + { + "name": "Foundation", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450470022/sfgwosxc2dgxo9yslapu.png", + "slug": "foundation", + "objectID": "56744722958ef13879b94fc2" + }, + { + "name": "LinkedIn", + "slug": "linkedin", + "objectID": "575ebcbada600b8ef43e51c4" + }, + { + "name": "planning", + "slug": "planning", + "objectID": "57ed528897eba84632db5b88" + }, + { + "name": "static", + "slug": "static", + "objectID": "57cbff559b3eb82e014a0364" + }, + { + "name": "Indie Maker", + "slug": "indie-maker", + "objectID": "5f1edf42cf3e61138dbef956" + }, + { + "name": "ThreeJS", + "slug": "threejs", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1520160600492/Bklw1UKOM.jpeg", + "objectID": "571fa589cfc14de85d6aca42" + }, + { + "name": "Yarn", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1477030779/nbhawthd7lervqjdiwrz.jpg", + "slug": "yarn", + "objectID": "5801b9c24c0f5aee780a3883" + }, + { + "name": "User Interface", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1462773835/uvhdwekyfkkh1tldkew7.jpg", + "slug": "user-interface", + "objectID": "56744721958ef13879b94823" + }, + { + "name": "fullstack", + "slug": "fullstack", + "objectID": "56744721958ef13879b94a6c" + }, + { + "name": "web performance", + "slug": "web-performance", + "objectID": "56744721958ef13879b94950" + }, + { + "name": "websockets", + "slug": "websockets", + "objectID": "56744721958ef13879b94a0f" + }, + { + "name": "SEO for Developers", + "slug": "seo-for-developers", + "objectID": "5f58d1c9ffbb8f35dd030cdd" + }, + { + "name": "graphic design", + "slug": "graphic-design", + "objectID": "56ab4801960088c21db4d845" + }, + { + "name": "bootstrap 4", + "slug": "bootstrap-4", + "objectID": "56744723958ef13879b953a4" + }, + { + "name": "push notifications", + "slug": "push-notifications", + "objectID": "577d40e61e03c69a78fb0dac" + }, + { + "name": "color", + "slug": "color", + "objectID": "5774aa8157675ec2fcfd0744" + }, + { + "name": "Scope", + "slug": "scope", + "objectID": "56f16b6cea857e0c6af05a4c" + }, + { + "name": "create-react-app", + "slug": "create-react-app", + "objectID": "58ec8cb535aeeb5330e71961" + }, + { + "name": "scalability", + "slug": "scalability", + "objectID": "5691193ecad8946e563c56e9" + }, + { + "name": "server hosting", + "slug": "server-hosting", + "objectID": "56744723958ef13879b9553e" + }, + { + "name": "login", + "slug": "login", + "objectID": "56b45894500fd79e29bd7bf4" + }, + { + "name": "Chat", + "slug": "chat", + "objectID": "575e6494ed4fa39df4f9af08" + }, + { + "name": "Culture", + "slug": "culture", + "objectID": "568a70511f77b14a93d83737" + }, + { + "name": "Recursion", + "slug": "recursion", + "objectID": "56903d0e91716a2d1dbadbca" + }, + { + "name": "cloudflare", + "slug": "cloudflare", + "objectID": "56744720958ef13879b947e6" + }, + { + "name": "whatsapp", + "slug": "whatsapp", + "objectID": "5732da8af311f7ed13dddcb3" + }, + { + "name": "Off Topic", + "slug": "off-topic", + "objectID": "575ab7852f07b512c4dce46e" + }, + { + "name": "passwords", + "slug": "passwords", + "objectID": "578395f816a33191db0432f4" + }, + { + "name": "map", + "slug": "map", + "objectID": "56fd21cd770db0f14a63ee67" + }, + { + "slug": "go-cjffccfnf0024tjs1mcwab09t", + "objectID": "5abf7c154496b1f745e95fce" + }, + { + "name": "Tailwind CSS Tutorial", + "slug": "tailwind-css-tutorial", + "objectID": "5f76e2947d160d41227d65b9" + }, + { + "name": "SQLite", + "slug": "sqlite", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516183050940/BJXG0cn4z.jpeg", + "objectID": "56d9e25a4aa5f35f09dd6c98" + }, + { + "name": "WebGL", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472831587/ajchcv7sjghl7p5k1tgm.jpg", + "slug": "webgl", + "objectID": "56744721958ef13879b94a3f" + }, + { + "name": "Phoenix framework", + "slug": "phoenix", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1522051540209/Hy20FXI5G.jpeg", + "objectID": "56744721958ef13879b94abc" + }, + { + "name": "magento 2", + "slug": "magento-2", + "objectID": "587789c03c79514bec516060" + }, + { + "name": "editors", + "slug": "editors", + "objectID": "56744723958ef13879b95262" + }, + { + "name": "google sheets", + "slug": "google-sheets", + "objectID": "56e669b622f645300192ed17" + }, + { + "name": "kafka", + "slug": "kafka", + "objectID": "572527cf5ec4095ed6f48bf3" + }, + { + "name": "Art", + "slug": "art", + "objectID": "56efa81abcca2d711e191eb9" + }, + { + "name": "generators", + "slug": "generators", + "objectID": "56744722958ef13879b950b8" + }, + { + "name": "Company", + "slug": "company", + "objectID": "572ca231bf97af427dd07e6c" + }, + { + "name": "console", + "slug": "console", + "objectID": "56744723958ef13879b952e1" + }, + { + "name": "Virtual Reality", + "slug": "virtual-reality", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1506666919/d9j2ku0cjlhrzrboojit.png", + "objectID": "56c87d289b87edaf6e25f825" + }, + { + "name": "apps", + "slug": "apps", + "objectID": "56744721958ef13879b94ac2" + }, + { + "name": "plugins", + "slug": "plugins", + "objectID": "56744723958ef13879b95204" + }, + { + "name": "terminal command", + "slug": "terminal-command", + "objectID": "5f6afc44cbf0b22e6d444142" + }, + { + "name": "arduino", + "slug": "arduino", + "objectID": "56744722958ef13879b951db" + }, + { + "name": "email marketing", + "slug": "email-marketing", + "objectID": "57b76044a629e4147b4251d5" + }, + { + "name": "project", + "slug": "project", + "objectID": "56744721958ef13879b94aae" + }, + { + "name": "3d", + "slug": "3d", + "objectID": "56744721958ef13879b94ad9" + }, + { + "name": "charts", + "slug": "charts", + "objectID": "56744720958ef13879b947d1" + }, + { + "name": "e-learning", + "slug": "e-learning", + "objectID": "569c9b4c72ca04ea5d79fc6c" + }, + { + "name": "browser", + "slug": "browser", + "objectID": "56744721958ef13879b94a11" + }, + { + "name": "snippets", + "slug": "snippets", + "objectID": "56744721958ef13879b948ae" + }, + { + "name": "Flux", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468113/cariy62rvjvlnz8ks7qw.png", + "slug": "flux", + "objectID": "56744721958ef13879b94d46" + }, + { + "name": "mac", + "slug": "mac", + "objectID": "56744721958ef13879b94a22" + }, + { + "name": "os", + "slug": "os", + "objectID": "568f6e425e7a940b3d3e0a92" + }, + { + "name": "integration", + "slug": "integration", + "objectID": "57f58a9917809963610207dd" + }, + { + "name": "logic", + "slug": "logic", + "objectID": "57b23c4cab585a4d6c1529cd" + }, + { + "name": "history", + "slug": "history", + "objectID": "572706c827ca2053d6613898" + }, + { + "name": "SOLID principles", + "slug": "solid-principles", + "objectID": "5f4dd1ae6f2d7874d4060e9b" + }, + { + "name": "Blogger", + "slug": "blogger-1", + "objectID": "5f2a4ee0d7d55f162b5da120" + }, + { + "name": "developer relations", + "slug": "developer-relations", + "objectID": "56744723958ef13879b953b6" + }, + { + "name": "Service Workers", + "slug": "service-workers", + "objectID": "56a746ba6e715c3c7fc5b7ef" + }, + { + "name": "iphone", + "slug": "iphone", + "objectID": "56744722958ef13879b95166" + }, + { + "name": "Parse", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1457160633/ybpgyd9fhrucyvgwda6a.png", + "slug": "parse", + "objectID": "56744722958ef13879b94efb" + }, + { + "slug": "sagar-jaybhay", + "objectID": "5cd9909c45e85c572ab538f7" + }, + { + "name": "design review", + "slug": "design-review", + "objectID": "5fb179420f6d4f4d2f66a32a" + }, + { + "name": "Career Coach", + "slug": "career-coach", + "objectID": "5f0bc6ef3fe8405bdb8d80be" + }, + { + "name": "hadoop", + "slug": "hadoop", + "objectID": "56744720958ef13879b94799" + }, + { + "name": "graph database", + "slug": "graph-database", + "objectID": "58b96527be993da9e4853150" + }, + { + "name": "continuous delivery", + "slug": "continuous-delivery", + "objectID": "56744721958ef13879b949a3" + }, + { + "name": "concurrency", + "slug": "concurrency", + "objectID": "56744723958ef13879b95312" + }, + { + "name": "compiler", + "slug": "compiler", + "objectID": "58790ce83c79514bec51631b" + }, + { + "name": "gsoc", + "slug": "gsoc", + "objectID": "56744721958ef13879b94dea" + }, + { + "name": "spa", + "slug": "spa", + "objectID": "56744721958ef13879b94d40" + }, + { + "name": "Collaboration", + "slug": "collaboration", + "objectID": "57d0839fb64935c2e8fdba94" + }, + { + "name": "Event Loop", + "slug": "event-loop", + "objectID": "56f7b7c59cad82b1e979026a" + }, + { + "name": "crud", + "slug": "crud", + "objectID": "56f71ff1aa013a5f87413652" + }, + { + "name": "Hoisting", + "slug": "hoisting", + "objectID": "56db37c9e853431899d03773" + }, + { + "name": "life-hack", + "slug": "life-hack", + "objectID": "5f96548740346172a86c2be7" + }, + { + "name": "mobile application design", + "slug": "mobile-application-design", + "objectID": "56744723958ef13879b95516" + }, + { + "name": "unix", + "slug": "unix", + "objectID": "56744721958ef13879b94a53" + }, + { + "name": "AdonisJS", + "slug": "adonisjs", + "objectID": "5770f47198002dc2b990254a" + }, + { + "name": "ecmascript6", + "slug": "ecmascript6", + "objectID": "56744720958ef13879b947db" + }, + { + "name": "stack", + "slug": "stack", + "objectID": "56744723958ef13879b95368" + }, + { + "slug": "cybersecurity", + "objectID": "593a98f803de49038fb02fd4" + }, + { + "name": "streaming", + "slug": "streaming", + "objectID": "56744722958ef13879b9505d" + }, + { + "name": "sysadmin", + "slug": "sysadmin", + "objectID": "56744721958ef13879b94aa2" + }, + { + "name": "build", + "slug": "build", + "objectID": "56744723958ef13879b95552" + }, + { + "name": "smart home", + "slug": "smart-home", + "objectID": "590d86fe042257bf29db782c" + }, + { + "name": "modules", + "slug": "modules", + "objectID": "56744722958ef13879b95197" + }, + { + "name": "CDN", + "slug": "cdn", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496755229/pmzin0lidq2ld88qeba5.png", + "objectID": "56744720958ef13879b947ae" + }, + { + "name": "#the-technical-writing-bootcamp", + "slug": "the-technical-writing-bootcamp-1", + "objectID": "5f732a92f955ec0a130f6290" + }, + { + "name": "Sublime Text", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1497046439/xny5lu0xfjzpfybrrl9c.png", + "slug": "sublime-text", + "objectID": "56744723958ef13879b95216" + }, + { + "name": "Ember.js", + "slug": "emberjs", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1498115063/txor4lfourtkcofjipii.png", + "objectID": "56744721958ef13879b94c17" + }, + { + "name": "Vuex", + "slug": "vuex", + "objectID": "580209af0c9f06220778a866" + }, + { + "name": "wordpress plugins", + "slug": "wordpress-plugins", + "objectID": "56744721958ef13879b94965" + }, + { + "name": "zsh", + "slug": "zsh", + "objectID": "56744723958ef13879b95202" + }, + { + "name": "recruitment", + "slug": "recruitment", + "objectID": "57b0b5a1fbdd622c03136428" + }, + { + "name": "Server side rendering", + "slug": "server-side-rendering", + "objectID": "5759222f462c2daddc9ac412" + }, + { + "name": "Roadmap", + "slug": "roadmap", + "objectID": "58cd6353557528fb61666e5d" + }, + { + "name": "hashnodebootcamp2", + "slug": "hashnodebootcamp2-1", + "objectID": "5faec06b7fcc8d387fc0d1a6" + }, + { + "name": "Polymer", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450468312/zwtljjmofmpplvho1wfa.png", + "slug": "polymer", + "objectID": "56744723958ef13879b954ab" + }, + { + "name": "Expo", + "slug": "expo", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1515394575880/SkuMI5gEM.jpeg", + "objectID": "58cb5f69ecb020d9744a6487" + }, + { + "name": "xml", + "slug": "xml", + "objectID": "56744721958ef13879b94b0b" + }, + { + "name": "tooling", + "slug": "tooling", + "objectID": "56744723958ef13879b95335" + }, + { + "name": "canvas", + "slug": "canvas", + "objectID": "56744722958ef13879b94f55" + }, + { + "name": "Backup", + "slug": "backup", + "objectID": "57df9e894a6aa43e72a98a15" + }, + { + "name": "Explain like I am five", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516175943338/SJ1IzFnVf.jpeg", + "slug": "explain-like-i-am-five", + "objectID": "5991e91f0bcf15061f140b7f" + }, + { + "name": "embedded", + "slug": "embedded", + "objectID": "571eb24785916079574f035e" + }, + { + "name": "bots", + "slug": "bots", + "objectID": "56f2726a35c92c494c5e3a73" + }, + { + "name": "Homebrew", + "slug": "homebrew", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502355995/yusx5q732shmiaypoq3f.png", + "objectID": "56744722958ef13879b951e9" + }, + { + "name": "webdesign", + "slug": "webdesign", + "objectID": "56744721958ef13879b949ec" + }, + { + "name": "styling", + "slug": "styling", + "objectID": "580515064c0f5aee780a3c9b" + }, + { + "name": "Mozilla", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1477481389/i3tfvov4fuqfkg2yeddv.png", + "slug": "mozilla", + "objectID": "56744721958ef13879b94c4f" + }, + { + "name": "javascript books", + "slug": "javascript-books", + "objectID": "56744723958ef13879b953fa" + }, + { + "name": "Atom", + "slug": "atom", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1497040963/kh7an2akihm9tf1w5ab2.png", + "objectID": "56744721958ef13879b94aa6" + }, + { + "name": "dev", + "slug": "dev", + "objectID": "56744721958ef13879b948bc" + }, + { + "name": "Best of Hashnode", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1556009843016/9mcKMnTI3.png", + "slug": "best-of-hashnode", + "objectID": "5c0c2ed6659f658d077550cf" + }, + { + "name": "Stack Overflow", + "slug": "stackoverflow", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1499949492/jff8br3fbln1yccb1tpb.png", + "objectID": "56744721958ef13879b949d7" + }, + { + "name": "progressive web apps", + "slug": "progressive-web-apps", + "objectID": "5702d00aabbcb496574bce11" + }, + { + "name": "animations", + "slug": "animations", + "objectID": "56744721958ef13879b948b4" + }, + { + "name": "translation", + "slug": "translation", + "objectID": "576ccb742d4c0ff55a8ae17a" + }, + { + "name": "desktop", + "slug": "desktop", + "objectID": "56744721958ef13879b948ce" + }, + { + "name": "habits", + "slug": "habits", + "objectID": "57e22bd48b1fca72b28833a4" + }, + { + "name": "#codingNewbies", + "slug": "codingnewbies", + "objectID": "5f7f9e43b8638504a7c122ed" + }, + { + "name": "google maps", + "slug": "google-maps", + "objectID": "57496c3892b151fb90adc735" + }, + { + "name": "back4app", + "slug": "back4app", + "objectID": "578bf0674416601b9574cb3b" + }, + { + "name": "Libraries", + "slug": "libraries", + "objectID": "568ecddf91716a2d1dbadb19" + }, + { + "name": "prototyping", + "slug": "prototyping", + "objectID": "56744723958ef13879b95241" + }, + { + "name": "Real Estate", + "slug": "real-estate", + "objectID": "56ee695b5edec9d7189a0be5" + }, + { + "name": "cache", + "slug": "cache", + "objectID": "567bfb342b926c3063c307dc" + }, + { + "name": "teaching", + "slug": "teaching", + "objectID": "56744723958ef13879b955b7" + }, + { + "name": "multithreading", + "slug": "multithreading", + "objectID": "56744723958ef13879b95300" + }, + { + "name": "opinion pieces", + "slug": "opinion-pieces", + "objectID": "5f0ffe5eaa660c1c354c06fc" + }, + { + "name": ".net core", + "slug": "net-core", + "objectID": "57d7d0d0f72dd3705c16014a" + }, + { + "name": "freelance", + "slug": "freelance", + "objectID": "56744722958ef13879b94e57" + }, + { + "name": "deployment automation", + "slug": "deployment-automation", + "objectID": "56744722958ef13879b95067" + }, + { + "name": "icon", + "slug": "icon", + "objectID": "56744723958ef13879b95289" + }, + { + "name": "Hashing", + "slug": "hashing", + "objectID": "591fd9bfe1cc498f829bf264" + }, + { + "name": "boilerplate", + "slug": "boilerplate", + "objectID": "56744723958ef13879b953b2" + }, + { + "name": "navigation", + "slug": "navigation", + "objectID": "574125dadf1e4d3563843066" + }, + { + "name": "Geospatial", + "slug": "geospatial", + "objectID": "5f25726a90ac4260edf35078" + }, + { + "name": "angular material", + "slug": "angular-material", + "objectID": "57c3ba45cb80370904fc5b48" + }, + { + "name": "ios apps", + "slug": "ios-apps", + "objectID": "56744721958ef13879b94ae2" + }, + { + "name": "wordpress themes", + "slug": "wordpress-themes", + "objectID": "56744721958ef13879b94af8" + }, + { + "name": "k8s", + "slug": "k8s", + "objectID": "58456f2afc2da7579e5f3ed0" + }, + { + "name": "Hugo", + "slug": "hugo", + "objectID": "57ce27e495368c463b09804f" + }, + { + "name": "a11y", + "slug": "a11y", + "objectID": "57aa00d170387a4ab0fe0cf8" + }, + { + "name": "webapps", + "slug": "webapps", + "objectID": "56744721958ef13879b94b6f" + }, + { + "name": "features", + "slug": "features", + "objectID": "56744722958ef13879b9515c" + }, + { + "name": "Prettier", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496483511/fdbnmvy2bkecx03csbom.png", + "slug": "prettier", + "objectID": "592d689fa6614cba3f738146" + }, + { + "name": "WebRTC", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1465362672/yzdode4h9er49uccfvbu.png", + "slug": "webrtc", + "objectID": "56744722958ef13879b94f0e" + }, + { + "name": "web developers", + "slug": "web-developers", + "objectID": "56744722958ef13879b94e6b" + }, + { + "name": "Emails", + "slug": "emails", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496150225/u6kgjtvvqkkefncoyovl.png", + "objectID": "57458b6c92b151fb90adc493" + }, + { + "name": "bundling", + "slug": "bundling", + "objectID": "5777dbd757675ec2fcfd09fb" + }, + { + "name": "localstorage", + "slug": "localstorage", + "objectID": "56744722958ef13879b95107" + }, + { + "name": "Earth Engine", + "slug": "earth-engine", + "objectID": "5f26246490ac4260edf3596e" + }, + { + "name": "test driven development", + "slug": "test-driven-development", + "objectID": "56744723958ef13879b95595" + }, + { + "name": "S3", + "slug": "s3", + "objectID": "588f13c9ae0398620533ed80" + }, + { + "name": "message queue", + "slug": "message-queue", + "objectID": "5688d3a00716b983ccc79766" + }, + { + "name": "mentor", + "slug": "mentor", + "objectID": "56744721958ef13879b94dc8" + }, + { + "name": "websites", + "slug": "websites", + "objectID": "56744721958ef13879b94c58" + }, + { + "name": "maven", + "slug": "maven", + "objectID": "56744723958ef13879b95232" + }, + { + "name": "turkish", + "slug": "turkish", + "objectID": "5f61e4c5dc74720d9b85ed19" + }, + { + "name": "MEAN Stack", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472484615/gnwpbhw8nqe9aj4frzzh.jpg", + "slug": "mean", + "objectID": "56744721958ef13879b94bc0" + }, + { + "name": "Emacs", + "slug": "emacs", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502978326/qzxw4oqebc9su0pzpvqt.png", + "objectID": "56744721958ef13879b949cf" + }, + { + "name": "Preact", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1459503980/i2tjk2olam4wqr7kyqet.jpg", + "slug": "preact", + "objectID": "56fe1c265db965849f7b379f" + }, + { + "name": "Future", + "slug": "future", + "objectID": "5699066c72ca04ea5d79faa1" + }, + { + "name": "es2015", + "slug": "es2015", + "objectID": "5678d29ae0956f4764b3edfb" + }, + { + "name": "sales", + "slug": "sales", + "objectID": "58cd06ec68e963fa61d68d7f" + }, + { + "name": "versioning", + "slug": "versioning", + "objectID": "578b9582b1a4a0d81ffbb1fe" + }, + { + "name": "computer", + "slug": "computer", + "objectID": "57628dcd820dd45f3fbd8eb6" + }, + { + "name": "cookies", + "slug": "cookies", + "objectID": "56744721958ef13879b94a7d" + }, + { + "name": "proxy", + "slug": "proxy", + "objectID": "56744721958ef13879b94917" + }, + { + "name": "Drupal", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1490298700/uqjjgtu4a1lpqxjcdshb.png", + "slug": "drupal", + "objectID": "57444da29ade925885158cb0" + }, + { + "name": "graphics", + "slug": "graphics", + "objectID": "578378ebfcb4d586db19492c" + }, + { + "name": "Scraping", + "slug": "scraping", + "objectID": "5834805addfa96eb7c5d478b" + }, + { + "name": "typography", + "slug": "typography", + "objectID": "56744721958ef13879b94944" + }, + { + "name": "marketplace", + "slug": "marketplace", + "objectID": "586d0df986a586aec93327e1" + }, + { + "name": "OOPS", + "slug": "oops", + "objectID": "5713f234162bdaad9f92b0c1" + }, + { + "name": "production", + "slug": "production", + "objectID": "57067a5e115103c3b097818b" + }, + { + "name": "process", + "slug": "process", + "objectID": "5694af13c1c0117cef5aea67" + }, + { + "name": "API basics ", + "slug": "api-basics", + "objectID": "5f8dd8dffc30613d8cd9379a" + }, + { + "name": "PaaS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1461694808/ip8ls4fz7nxi01uhvmch.jpg", + "slug": "paas", + "objectID": "56744721958ef13879b94ddc" + }, + { + "name": "Website design", + "slug": "website-design", + "objectID": "5866a4c0b99398bc30c43daa" + }, + { + "name": "SSR", + "slug": "ssr", + "objectID": "5747fbbd9ade925885158f94" + }, + { + "name": "i18n", + "slug": "i18n", + "objectID": "568f1af6525da8063d08fb2d" + }, + { + "name": "ci", + "slug": "ci", + "objectID": "56744721958ef13879b94a16" + }, + { + "name": "centos", + "slug": "centos", + "objectID": "57a67d66e6998a66b06f40e6" + }, + { + "name": "social", + "slug": "social", + "objectID": "5709b8c3115103c3b0978327" + }, + { + "slug": "go-cjidm6n1p00lpq9s29dy2bsiq", + "objectID": "5b218969e0d20c016e052f69" + }, + { + "name": "patterns", + "slug": "patterns", + "objectID": "56744721958ef13879b94db8" + }, + { + "name": "workathome", + "slug": "workathome", + "objectID": "5f19d647cef915427a14ca2c" + }, + { + "name": "selenium-webdriver", + "slug": "selenium-webdriver-1", + "objectID": "5f0c3b23880268625262ba76" + }, + { + "name": "macbook", + "slug": "macbook", + "objectID": "56744721958ef13879b94dc2" + }, + { + "name": "Voice", + "slug": "voice", + "objectID": "590102fd9863a67f4cc93055" + }, + { + "name": "orm", + "slug": "orm", + "objectID": "56b632b3a0967efc587c7d24" + }, + { + "name": "Bitbucket", + "slug": "bitbucket", + "objectID": "580e08175fec191d85b14fc7" + }, + { + "name": "dashboard", + "slug": "dashboard", + "objectID": "56b45894500fd79e29bd7bf3" + }, + { + "name": "composer", + "slug": "composer", + "objectID": "56b234f2a71b2df12bea6e43" + }, + { + "name": "Remote Sensing ", + "slug": "remote-sensing", + "objectID": "5f25726a90ac4260edf35077" + }, + { + "name": "ELM", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1491295764/mh4haipogztgffbnt4y4.png", + "slug": "elm", + "objectID": "567bbdf52b926c3063c30713" + }, + { + "name": "spark", + "slug": "spark", + "objectID": "56744722958ef13879b95180" + }, + { + "name": "ionic framework", + "slug": "ionic-framework", + "objectID": "56744723958ef13879b95254" + }, + { + "name": "robotics", + "slug": "robotics", + "objectID": "56744723958ef13879b953a2" + }, + { + "name": "twilio", + "slug": "twilio", + "objectID": "57e57691ef99cf03582fe2b3" + }, + { + "name": "mvp", + "slug": "mvp", + "objectID": "56744723958ef13879b95572" + }, + { + "name": "medium", + "slug": "medium", + "objectID": "56744721958ef13879b94871" + }, + { + "slug": "devjourney", + "objectID": "5e43fc8b8c89a92316ccd6c2" + }, + { + "name": "azure certified", + "slug": "azure-certified", + "objectID": "5f28ea6e3e336e0de23093c0" + }, + { + "name": "PostCSS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1459504796/nipxkl4fu2zf7sqfl5fj.jpg", + "slug": "postcss", + "objectID": "56744721958ef13879b94e29" + }, + { + "name": "AR", + "slug": "ar", + "objectID": "586cd5ae615b9737b81b3ddb" + }, + { + "name": "photoshop", + "slug": "photoshop", + "objectID": "5674471d958ef13879b94796" + }, + { + "name": "crm", + "slug": "crm", + "objectID": "580df8332a45c6fdcb43fa14" + }, + { + "name": "funny", + "slug": "funny", + "objectID": "56744723958ef13879b9547b" + }, + { + "name": "Frontend frameworks", + "slug": "frontend-frameworks", + "objectID": "56a0676792921b8f79d360f5" + }, + { + "name": "technology stack", + "slug": "technology-stack", + "objectID": "56b99e6cacee1cee848702ec" + }, + { + "name": "jekyll", + "slug": "jekyll", + "objectID": "56744721958ef13879b948e8" + }, + { + "name": "cloudinary", + "slug": "cloudinary", + "objectID": "5678a007e0956f4764b3ed53" + }, + { + "name": "queue", + "slug": "queue", + "objectID": "56744723958ef13879b952c0" + }, + { + "name": "sdk", + "slug": "sdk", + "objectID": "56f972afea33a5b266f2fe04" + }, + { + "name": "styleguide", + "slug": "styleguide", + "objectID": "56744722958ef13879b951a4" + }, + { + "name": "Meta", + "slug": "meta", + "objectID": "58b6c12eb2566b537ac16cb7" + }, + { + "name": "CORS", + "slug": "cors", + "objectID": "5676154ae64b075af6ade54e" + }, + { + "name": "props", + "slug": "props", + "objectID": "5f2959166face9141b78fa82" + }, + { + "name": "Aurelia", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1453819641/j5c2dwhqwvzh9apczioe.jpg", + "slug": "aurelia", + "objectID": "56744722958ef13879b94f49" + }, + { + "name": "YAML", + "slug": "yaml", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1499159858/ude93xlquvvbxbw5xkg4.png", + "objectID": "56d9941a489cf60d99aa90c4" + }, + { + "name": "EQCSS", + "slug": "eqcss", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1520491825399/HJF4pLCdz.png", + "objectID": "5784baeefcb4d586db194a64" + }, + { + "name": "layout", + "slug": "layout", + "objectID": "56d2f72f1878dfef04178e6e" + }, + { + "name": "flow", + "slug": "flow", + "objectID": "56744721958ef13879b94a2e" + }, + { + "name": "admin", + "slug": "admin", + "objectID": "57778738f271844db9e1eb41" + }, + { + "name": "tech", + "slug": "tech-cilba77mg0010ya53d05qtkuu", + "objectID": "56d7498b6722ee828dbeafe3" + }, + { + "name": "Cordova", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1520160966692/ByyCeLt_M.jpeg", + "slug": "cordova", + "objectID": "56744721958ef13879b94a1b" + }, + { + "name": "Build tool", + "slug": "build-tool", + "objectID": "56744722958ef13879b950a3" + }, + { + "name": "vps", + "slug": "vps", + "objectID": "56744722958ef13879b951c4" + }, + { + "name": "gradle", + "slug": "gradle", + "objectID": "56744722958ef13879b95164" + }, + { + "name": "ebook", + "slug": "ebook", + "objectID": "56744721958ef13879b948f0" + }, + { + "slug": "hooks", + "objectID": "5c1778c2252f6d5b707ae169" + }, + { + "name": "gmail", + "slug": "gmail", + "objectID": "58596eaaeb509c3ba23d4c87" + }, + { + "name": "inheritance", + "slug": "inheritance", + "objectID": "573349a7181d813d33746639" + }, + { + "name": "stripe", + "slug": "stripe", + "objectID": "56744723958ef13879b9554c" + }, + { + "name": "#sucessful blogging", + "slug": "sucessful-blogging", + "objectID": "5fb801781b7ab0041800c67c" + }, + { + "name": "watercooler", + "slug": "watercooler", + "objectID": "5f36e920877a013acb03cd10" + }, + { + "name": "eloquent", + "slug": "eloquent", + "objectID": "56ed7b765edec9d7189a0b73" + }, + { + "name": "image", + "slug": "image", + "objectID": "56744721958ef13879b948fc" + }, + { + "name": "book", + "slug": "book", + "objectID": "56744720958ef13879b947b2" + }, + { + "name": "router", + "slug": "router", + "objectID": "56744723958ef13879b95210" + }, + { + "name": "#ChooseToChallenge", + "slug": "choosetochallenge", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1614605641484/1qeXO9QXg.png", + "objectID": "603cc4b61f91337d465bee68" + }, + { + "name": "geemap", + "slug": "geemap", + "objectID": "5f465bac9b597625e2dec06a" + }, + { + "name": "ASP", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1451108441/qt4zgtcynwzy2rjvk0t6.png", + "slug": "asp", + "objectID": "5674471d958ef13879b9477c" + }, + { + "name": "front end", + "slug": "front-end", + "objectID": "56744723958ef13879b95554" + }, + { + "name": "SVG Animation", + "slug": "svg-animation", + "objectID": "569cd00972ca04ea5d79fca2" + }, + { + "name": "meteorjs", + "slug": "meteorjs", + "objectID": "56744723958ef13879b9558f" + }, + { + "name": "nest", + "slug": "nest", + "objectID": "583ca6c6ddfa96eb7c5d896f" + }, + { + "name": "podcasts", + "slug": "podcasts", + "objectID": "56744722958ef13879b95194" + }, + { + "name": "designing", + "slug": "designing", + "objectID": "56744721958ef13879b94bd9" + }, + { + "name": "Clerk.dev", + "slug": "clerkdev", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1625245518596/nW4Y4hYHH.png", + "objectID": "60df384f03707d644a4feb38" + }, + { + "name": "web servers", + "slug": "web-servers", + "objectID": "56744721958ef13879b94a88" + }, + { + "name": "function", + "slug": "function", + "objectID": "56744720958ef13879b947ea" + }, + { + "name": "DraftJS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1491387822/zwctxp8006exywfg17pd.jpg", + "slug": "draftjs", + "objectID": "56f4d674990bca7c25e99318" + }, + { + "name": "redux-saga", + "slug": "redux-saga", + "objectID": "5776f09cf271844db9e1eb05" + }, + { + "name": "responsive designs", + "slug": "responsive-designs", + "objectID": "56744721958ef13879b94d5b" + }, + { + "name": "Socket.io", + "slug": "socketio-cijy9e2c700c6vm5357q8xsf3", + "objectID": "56aa0ea0960088c21db4d77a" + }, + { + "name": "OSS", + "slug": "oss", + "objectID": "581875942ca37f164781f4b1" + }, + { + "name": "chartjs", + "slug": "chartjs", + "objectID": "56744721958ef13879b94993" + }, + { + "slug": "deno", + "objectID": "5cca9dd21077bc6278d31cc7" + }, + { + "slug": "cisco", + "objectID": "5d9cc879f74b4d4660eede6b" + }, + { + "name": "emoji", + "slug": "emoji", + "objectID": "571751b03c2a84abc85a1e11" + }, + { + "name": "await", + "slug": "await", + "objectID": "56cbdb23b70682283f9edeb7" + }, + { + "name": "hibernate", + "slug": "hibernate", + "objectID": "56744723958ef13879b955ac" + }, + { + "name": "Julia", + "slug": "julia", + "objectID": "58749cfee6e8728a7f133535" + }, + { + "name": "vagrant", + "slug": "vagrant", + "objectID": "56744721958ef13879b94a24" + }, + { + "name": "grid", + "slug": "grid", + "objectID": "56744723958ef13879b952d3" + }, + { + "name": "naming", + "slug": "naming", + "objectID": "5747655e92b151fb90adc622" + }, + { + "name": "error", + "slug": "error", + "objectID": "56744721958ef13879b9496b" + }, + { + "name": "templates", + "slug": "templates", + "objectID": "56744721958ef13879b94853" + }, + { + "name": "design and architecture", + "slug": "design-and-architecture", + "objectID": "5f38bd060801bf3f76e5f9e5" + }, + { + "name": "Haskell", + "slug": "haskell", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496182720/z8gaemi99htfdnclmicj.png", + "objectID": "56744723958ef13879b9537a" + }, + { + "name": "PayPal", + "slug": "paypal", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1504679615/ds7ftsav58hjetqeeqq3.jpg", + "objectID": "56ee65cfcb06805ba9b7c66d" + }, + { + "name": "native", + "slug": "native", + "objectID": "56744723958ef13879b9530a" + }, + { + "name": "maps", + "slug": "maps", + "objectID": "574853c092b151fb90adc6b1" + }, + { + "name": "class", + "slug": "class", + "objectID": "573c6a7803e642f04bb03d47" + }, + { + "name": "mobile application development", + "slug": "mobile-application-development", + "objectID": "56744721958ef13879b949b7" + }, + { + "name": "The Clerk Hackathon on Hashnode", + "slug": "clerkhackathon", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1625245553278/S6gbVfdNp.png", + "objectID": "60df381403707d644a4feb2f" + }, + { + "name": "web3", + "slug": "web3", + "logo": null, + "objectID": "59df443dfb1deef9745a4ef0" + }, + { + "slug": "wsl", + "objectID": "595ed5ae8f1dffe434c00000" + }, + { + "name": "geolocation", + "slug": "geolocation", + "objectID": "579f2a6bb5724a7273404206" + }, + { + "name": "coroutines", + "slug": "coroutines", + "objectID": "56facb5fbac95334fc2fa50b" + }, + { + "name": "object", + "slug": "object", + "objectID": "56744722958ef13879b9505b" + }, + { + "name": "debug", + "slug": "debug", + "objectID": "56744721958ef13879b94922" + }, + { + "name": "freelancer", + "slug": "freelancer", + "objectID": "56744723958ef13879b9550a" + }, + { + "name": "Cosmic JS", + "slug": "cosmic-js", + "objectID": "590743c50e14932382c2ad5a" + }, + { + "name": "WhoIsHiring", + "slug": "whoishiring", + "objectID": "5d946e4ec510092a323bc34a" + }, + { + "name": "ide", + "slug": "ide", + "objectID": "56744721958ef13879b94879" + }, + { + "name": "pair programming", + "slug": "pair-programming", + "objectID": "56744722958ef13879b95071" + }, + { + "slug": "health-cjaeh844x02vvo3wtj5r2s75q", + "objectID": "5a189c9fee67ea9312f02c18" + }, + { + "name": "code smell", + "slug": "code-smell", + "objectID": "57361d1cffaaff8febd12cee" + }, + { + "name": "V8", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1450536374/hnlihf5tv3veoxx1igpa.jpg", + "slug": "v8", + "objectID": "56744723958ef13879b954f0" + }, + { + "name": "Erlang", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475321105/tp4co0lnolmi4x7ln7f6.jpg", + "slug": "erlang", + "objectID": "56744722958ef13879b94e60" + }, + { + "name": "Clojure", + "slug": "clojure", + "objectID": "56b01bce0a7ca0c6f70c1ef8" + }, + { + "name": "rabbitmq", + "slug": "rabbitmq", + "objectID": "56a4fc8ec84f2c6913b8e9f9" + }, + { + "name": "Sketch", + "slug": "sketch", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1522266937699/ByGS7dtcG.jpeg", + "objectID": "56744722958ef13879b94e7d" + }, + { + "name": "backend as a service", + "slug": "backend-as-a-service", + "objectID": "577f08da16a33191db042f9e" + }, + { + "name": "mocha", + "slug": "mocha", + "objectID": "56744721958ef13879b94a3a" + }, + { + "name": "stream", + "slug": "stream", + "objectID": "56744723958ef13879b95580" + }, + { + "name": "container", + "slug": "container", + "objectID": "56744721958ef13879b94ad6" + }, + { + "name": "woocommerce", + "slug": "woocommerce", + "objectID": "56744720958ef13879b94808" + }, + { + "name": "webperf", + "slug": "webperf-ciur6tor503mfpx53ic2rvrs2", + "objectID": "5810e609a901f605c438b691" + }, + { + "name": "form", + "slug": "form", + "objectID": "56744722958ef13879b95138" + }, + { + "name": "#⛺the-technical-writing-bootcamp", + "slug": "the-technical-writing-bootcamp", + "objectID": "5f6d12a1005ded5336f6f534" + }, + { + "name": "HTML Emails", + "slug": "html-emails", + "objectID": "56a1b72a72ca04ea5d7a003b" + }, + { + "name": "PHPUnit", + "slug": "phpunit", + "objectID": "57ea3f2397eba84632db561a" + }, + { + "name": "http2", + "slug": "http2", + "objectID": "56744721958ef13879b94a76" + }, + { + "name": "kibana", + "slug": "kibana", + "objectID": "56744721958ef13879b9486d" + }, + { + "name": "osx", + "slug": "osx", + "objectID": "56744723958ef13879b9523e" + }, + { + "name": "ghost", + "slug": "ghost", + "objectID": "56744722958ef13879b951c6" + }, + { + "name": "hybrid apps", + "slug": "hybrid-apps", + "objectID": "56744721958ef13879b94e08" + }, + { + "name": "virtual dom", + "slug": "virtual-dom", + "objectID": "56744720958ef13879b947fc" + }, + { + "name": "editor", + "slug": "editor", + "objectID": "5674471d958ef13879b94781" + }, + { + "name": "Session", + "slug": "session", + "objectID": "57c8241860189c8953a67f81" + }, + { + "name": "parse server", + "slug": "parse-server", + "objectID": "578ae4e4b1a4a0d81ffbb1bb" + }, + { + "slug": "tailwind", + "objectID": "5ddd484e94c050e177a6aa7e" + }, + { + "name": "mongo", + "slug": "mongo", + "objectID": "56744721958ef13879b94a93" + }, + { + "name": "what successful blogging means to me", + "slug": "what-successful-blogging-means-to-me", + "objectID": "5faff31939a1f54636490632" + }, + { + "name": "windows server", + "slug": "windows-server", + "objectID": "5f1dd296f4016901885ccbf8" + }, + { + "name": "Objective C", + "slug": "objective-c", + "objectID": "56744721958ef13879b94bfe" + }, + { + "name": "vr", + "slug": "vr", + "objectID": "5674d5807446b75bb60141f8" + }, + { + "name": "microsoft edge", + "slug": "microsoft-edge", + "objectID": "56744720958ef13879b9480c" + }, + { + "name": "zurb", + "slug": "zurb", + "objectID": "56744721958ef13879b94a36" + }, + { + "name": "promise", + "slug": "promise", + "objectID": "56744721958ef13879b9488b" + }, + { + "slug": "growth", + "objectID": "5a64fbe6e30c5b6655a6a4df" + }, + { + "name": "Meetup", + "slug": "meetup", + "objectID": "56d9b1b0e853431899d036ce" + }, + { + "name": "modal", + "slug": "modal", + "objectID": "56ace1e6cc975f0cc6878bc0" + }, + { + "name": "Benchmark", + "slug": "benchmark", + "objectID": "5680fde5aeae5c9e229cf8e1" + }, + { + "name": "Lua", + "slug": "lua", + "objectID": "5726e4fac1f71f91e880ad2b" + }, + { + "name": "perl", + "slug": "perl", + "objectID": "56744722958ef13879b9512e" + }, + { + "name": "postgres", + "slug": "postgres", + "objectID": "56744722958ef13879b94f0b" + }, + { + "name": "Element Queries", + "slug": "element-queries", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1498763362/lvxxrbdpyjwm1c8pxjck.png", + "objectID": "581a55d4c055bbfb46d880da" + }, + { + "name": "logstash", + "slug": "logstash", + "objectID": "56744723958ef13879b953c3" + }, + { + "name": "FaaS", + "slug": "faas", + "objectID": "58cbe70848830eae2c11fdf4" + }, + { + "name": "laravel ", + "slug": "laravel-cikr40o0m01r27453d8eux03p", + "objectID": "56c4ad109c7666b0da73f29d" + }, + { + "name": "immutable", + "slug": "immutable", + "objectID": "56744722958ef13879b9514a" + }, + { + "slug": "pmlcourse", + "objectID": "5e4a6b728c89a92316cd4a33" + }, + { + "name": "alternative", + "slug": "alternative", + "objectID": "58085c202a45c6fdcb43f3c3" + }, + { + "name": "Smalltalk", + "slug": "smalltalk", + "objectID": "57da642fd17cab545caba0d3" + }, + { + "name": "cpu", + "slug": "cpu", + "objectID": "57ae11c08dae0c2f1d4420cb" + }, + { + "name": "survey", + "slug": "survey", + "objectID": "56744721958ef13879b949c2" + }, + { + "name": "Cassandra", + "slug": "cassandra", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516175375653/BJdMlF34z.jpeg", + "objectID": "56744721958ef13879b9490e" + }, + { + "name": "css3 animation", + "slug": "css3-animation", + "objectID": "56744722958ef13879b94ef0" + }, + { + "name": "Semantic UI", + "slug": "semantic-ui", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1496405644/rsuq8bqv2aoqqnq8ckzw.png", + "objectID": "56744723958ef13879b95206" + }, + { + "name": "restful", + "slug": "restful", + "objectID": "56744723958ef13879b952c6" + }, + { + "name": "Deploy ", + "slug": "deploy", + "objectID": "57578b6282cbbab8dcd47842" + }, + { + "name": "solid", + "slug": "solid", + "objectID": "56e6d5598c0bb8288a559c97" + }, + { + "name": "font awesome", + "slug": "font-awesome", + "objectID": "56744721958ef13879b9492f" + }, + { + "slug": "flutter-cjxern4nz000zx6s1d95hxw7x", + "objectID": "5d14d342867d9aba094fd8f5" + }, + { + "slug": "nestjs", + "objectID": "59e46480ebcd60373ac04db3" + }, + { + "name": "junit", + "slug": "junit", + "objectID": "57935f8804cd973c9154652c" + }, + { + "name": "TLS", + "slug": "tls", + "objectID": "56a6742dc84f2c6913b8eac2" + }, + { + "name": "NetworkAutomation", + "slug": "networkautomation", + "objectID": "5f9da80a701b426a980950db" + }, + { + "name": "Less", + "slug": "less", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1509610482/o0vybjlg9bncpy4tq0x0.png", + "objectID": "56744721958ef13879b949ef" + }, + { + "name": "bdd", + "slug": "bdd", + "objectID": "56744721958ef13879b94aa0" + }, + { + "name": "baas", + "slug": "baas", + "objectID": "56744723958ef13879b953ad" + }, + { + "name": "MVVM", + "slug": "mvvm", + "objectID": "56a0ee5172ca04ea5d79ff9d" + }, + { + "name": "responsive", + "slug": "responsive", + "objectID": "56744723958ef13879b95520" + }, + { + "name": "Error Tracking", + "slug": "error-tracking", + "objectID": "58d2b7fa440c92dcfd4c5801" + }, + { + "name": "media queries", + "slug": "media-queries", + "objectID": "56744721958ef13879b949f2" + }, + { + "slug": "2articles1week-1", + "objectID": "5f0b171bf80d68509e50d2c1" + }, + { + "name": "RethinkDB", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1455115223/oluebzm7a23ayicyyr93.png", + "slug": "rethinkdb", + "objectID": "5674471d958ef13879b94774" + }, + { + "name": ".NET", + "slug": "net-cikag7ck9004u4153550rzs6c", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1515074840143/Bkl7B3sXz.jpeg", + "objectID": "56b54dae8dabdc6142c1ac87" + }, + { + "name": "codeigniter", + "slug": "codeigniter", + "objectID": "577d5a59f5d62870bc1e3436" + }, + { + "name": "web dev", + "slug": "web-dev", + "objectID": "56744722958ef13879b951f5" + }, + { + "name": "Question", + "slug": "question", + "objectID": "56b4ee44ed97cf2d3faa9e85" + }, + { + "name": "passport", + "slug": "passport", + "objectID": "56744723958ef13879b955b5" + }, + { + "slug": "strapi", + "objectID": "5a60b356acaaf63131a26558" + }, + { + "name": "ECS", + "slug": "ecs", + "objectID": "58456f2afc2da7579e5f3ece" + }, + { + "name": "Motivation ", + "slug": "motivation-1", + "objectID": "5f95c76540346172a86c28c1" + }, + { + "name": "KoaJS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472485426/mypuzb6iv30nivcnj67f.jpg", + "slug": "koa", + "objectID": "56744720958ef13879b947fb" + }, + { + "name": "HapiJS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472485161/dtjd0iyqgwiqqksg3see.png", + "slug": "hapijs", + "objectID": "56744721958ef13879b94dd2" + }, + { + "name": "Java Framework", + "slug": "java-framework", + "objectID": "5674471d958ef13879b9476f" + }, + { + "name": "NativeScript", + "slug": "nativescript", + "objectID": "578f329a5460288cdeb6f281" + }, + { + "name": "realtime apps", + "slug": "realtime-apps", + "objectID": "56744721958ef13879b94a1e" + }, + { + "name": "DevRant", + "slug": "devrant", + "objectID": "5d946e601971c92f3298b281" + }, + { + "name": "amp", + "slug": "amp", + "objectID": "56744723958ef13879b9556c" + }, + { + "name": "grunt", + "slug": "grunt", + "objectID": "56744723958ef13879b9547f" + }, + { + "name": "es5", + "slug": "es5", + "objectID": "56744722958ef13879b94e5a" + }, + { + "name": "servers", + "slug": "servers", + "objectID": "56744722958ef13879b94e49" + }, + { + "name": "rss", + "slug": "rss", + "objectID": "56744721958ef13879b949e6" + }, + { + "slug": "flask-cje4g3tgk00wdm0wtaepqxd29", + "objectID": "5a94378b2e2d22686d3319ec" + }, + { + "slug": "vpn", + "objectID": "5a66e6714c88fdb11626d866" + }, + { + "name": "writing ", + "slug": "writing-1", + "objectID": "5f541f8fd34e0b0a2135b7ac" + }, + { + "name": "CouchDB", + "slug": "couchdb", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1516182897537/HJ9_TqnNG.jpeg", + "objectID": "56744722958ef13879b94e52" + }, + { + "name": "responsive design", + "slug": "responsive-design", + "objectID": "568104b15d0b198322f23be3" + }, + { + "name": "functional", + "slug": "functional", + "objectID": "56744723958ef13879b9541e" + }, + { + "name": "es7", + "slug": "es7", + "objectID": "56744722958ef13879b9516e" + }, + { + "name": "flowtype", + "slug": "flowtype", + "objectID": "57a07b7703626115baea275d" + }, + { + "name": "airbnb", + "slug": "airbnb", + "objectID": "56744721958ef13879b9495f" + }, + { + "slug": "swiftui", + "objectID": "5d117acd15a6b27b36bb063b" + }, + { + "name": "offline", + "slug": "offline", + "objectID": "57ff8bed7a5d253b23bc40dd" + }, + { + "name": "css preprocessors", + "slug": "css-preprocessors", + "objectID": "56744723958ef13879b95314" + }, + { + "name": "web app", + "slug": "web-app", + "objectID": "56744722958ef13879b950de" + }, + { + "name": "beta", + "slug": "beta", + "objectID": "56c6bd7d46a50cb768ba7d04" + }, + { + "name": "webdriver", + "slug": "webdriver", + "objectID": "56a1bb2a92921b8f79d3620e" + }, + { + "name": "Algolia", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1454497142/tmtr6swfz0tqfeiphd0q.png", + "slug": "algolia", + "objectID": "56744723958ef13879b95404" + }, + { + "name": "tech stacks", + "slug": "tech-stacks", + "objectID": "56744721958ef13879b94aea" + }, + { + "name": "relay", + "slug": "relay", + "objectID": "56744720958ef13879b947a8" + }, + { + "name": "Sequelize", + "slug": "sequelize", + "objectID": "56bf8908f7a8a564cd3cf417" + }, + { + "name": "CoffeeScript", + "slug": "coffeescript", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1524116531939/ry2EnsS2M.jpeg", + "objectID": "56744722958ef13879b9519f" + }, + { + "name": "browserify", + "slug": "browserify", + "objectID": "56744721958ef13879b94c51" + }, + { + "slug": "rtos", + "objectID": "5e94317328f1a84f59c49fb9" + }, + { + "slug": "spanish", + "objectID": "5d24dd07963b3099469e31b1" + }, + { + "name": "universal", + "slug": "universal", + "objectID": "5691098591906f99ef523690" + }, + { + "name": "software design", + "slug": "software-design", + "objectID": "56744721958ef13879b94acd" + }, + { + "name": "CSS Modules", + "slug": "css-modules", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1502977775/w0xxhrabmj1zhddsdiu1.png", + "objectID": "56bf8908f7a8a564cd3cf415" + }, + { + "name": "PhpStorm", + "slug": "phpstorm", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1497046152/nmpeb8i0lo2zofxg7xo5.png", + "objectID": "56eae87928492b76a9948344" + }, + { + "name": "scaling", + "slug": "scaling", + "objectID": "56744721958ef13879b94aa9" + }, + { + "name": "tool", + "slug": "tool", + "objectID": "568bb9dbe99c5444f3233892" + }, + { + "name": "charting library", + "slug": "charting-library", + "objectID": "56744721958ef13879b94e41" + }, + { + "slug": "devblog", + "objectID": "5cdbcce2d7898f811504a6c9" + }, + { + "name": "IWD2021", + "slug": "iwd2021", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1614605663765/dl9O9JyP9.png", + "objectID": "603cecbcc8eb04017922ce83" + }, + { + "slug": "cpp-ck4ra5k7300nlv2s1jbkdp2qh", + "objectID": "5e08e075bcc8c0ce78e93263" + }, + { + "name": "smtp", + "slug": "smtp", + "objectID": "56744723958ef13879b953c9" + }, + { + "name": "plugin", + "slug": "plugin", + "objectID": "56744722958ef13879b94ff8" + }, + { + "name": "cto", + "slug": "cto", + "objectID": "56744720958ef13879b9480f" + }, + { + "name": "100DaysOfCloud", + "slug": "100daysofcloud", + "objectID": "5f216568938147308462a35b" + }, + { + "name": "PhoneGap", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1475235526/igis5i1twypixaebdkun.jpg", + "slug": "phonegap", + "objectID": "56744720958ef13879b947fa" + }, + { + "name": "SailsJS", + "logo": "https://res.cloudinary.com/hashnode/image/upload/v1472484652/puabwwilk0dvwv9gsepb.png", + "slug": "sailsjs", + "objectID": "56744723958ef13879b9527a" + }, + { + "name": "socket", + "slug": "socket", + "objectID": "576bd575956de5c931689074" + }, + { + "name": "wasm", + "slug": "wasm", + "objectID": "57612cfa7e4505f8314fb29a" + }, + { + "name": "rxjava", + "slug": "rxjava", + "objectID": "56d93d14696d94e491c06f47" + }, + { + "name": "Testing Library", + "slug": "testing-library", + "logo": "https://cdn.hashnode.com/res/hashnode/image/upload/v1618896704282/9Z3cbqhmn.png", + "objectID": "607e6751eb2bd30d2d22a556" + }, + { + "name": "c#", + "slug": "c-cikbdqjwh0042l553122kmxlz", + "objectID": "56b629b2e6740d0959b6f3d9" + }, + { + "name": "Alexa", + "slug": "alexa", + "objectID": "57bb2f081351c2290bba1d24" + }, + { + "name": "mern-stack", + "slug": "mern-stack", + "objectID": "56c752ab34d45a99221aa34f" + }, + { + "name": "microservice", + "slug": "microservice", + "objectID": "56744723958ef13879b95421" + }, + { + "name": "lodash", + "slug": "lodash", + "objectID": "56744722958ef13879b95162" + }, + { + "name": "code splitting", + "slug": "code-splitting", + "objectID": "56e17a0f5d4f204da59e0058" + }, + { + "name": "GraphQL ", + "slug": "graphql-cintl8ori01p0y353nth5857g", + "objectID": "572a9b9f109fb69b463406e9" + }, + { + "name": "isomorphic apps", + "slug": "isomorphic-apps", + "objectID": "56744723958ef13879b95505" + }, + { + "name": "internet explorer", + "slug": "internet-explorer", + "objectID": "56744721958ef13879b94c7b" + }, + { + "name": "mobile app", + "slug": "mobile-app", + "objectID": "576934c7a841f03b9338c6b3" + } +] \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts index a28d1de0..188b0561 100644 --- a/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/article/medium.provider.ts @@ -1,28 +1,73 @@ -import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface"; +import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface'; +import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto'; export class MediumProvider implements ArticleProvider { - identifier = 'medium'; - name = 'Medium'; + identifier = 'medium'; + name = 'Medium'; - async authenticate(token: string) { - const {data: {name, id, imageUrl}} = await (await fetch('https://api.medium.com/v1/me', { - headers: { - Authorization: `Bearer ${token}` - } - })).json(); + async authenticate(token: string) { + const { + data: { name, id, imageUrl }, + } = await ( + await fetch('https://api.medium.com/v1/me', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + ).json(); - return { - id, - name, - token, - picture: imageUrl + return { + id, + name, + token, + picture: imageUrl, + }; + } + + async publications(token: string) { + const { id } = await this.authenticate(token); + const { data } = await ( + await fetch(`https://api.medium.com/v1/users/${id}/publications`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + ).json(); + + return data; + } + + async post(token: string, content: string, settings: MediumSettingsDto) { + const { id } = await this.authenticate(token); + const { data, ...all } = await ( + await fetch( + settings?.publication + ? `https://api.medium.com/v1/publications/${settings?.publication}/posts` + : `https://api.medium.com/v1/users/${id}/posts`, + { + method: 'POST', + body: JSON.stringify({ + title: settings.title, + contentFormat: 'markdown', + content, + ...(settings.canonical ? { canonicalUrl: settings.canonical } : {}), + ...(settings?.tags?.length + ? { tags: settings?.tags?.map(p => p.value) } + : {}), + publishStatus: settings?.publication ? 'draft' : 'public', + }), + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, } - } + ) + ).json(); - async post(token: string, content: string, settings: object) { - return { - postId: '123', - releaseURL: 'https://dev.to' - } - } -} \ No newline at end of file + console.log(all); + return { + postId: data.id, + releaseURL: data.url, + }; + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index b04aea39..682fafb6 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -1,96 +1,217 @@ -import {AuthTokenDetails, PostDetails, PostResponse, SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface"; -import {makeId} from "@gitroom/nestjs-libraries/services/make.is"; +import { + AuthTokenDetails, + PostDetails, + PostResponse, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; export class RedditProvider implements SocialProvider { - identifier = 'reddit'; - name = 'Reddit'; - async refreshToken(refreshToken: string): Promise<AuthTokenDetails> { - const {access_token: accessToken, refresh_token: newRefreshToken, expires_in: expiresIn} = await (await fetch('https://www.reddit.com/api/v1/access_token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString('base64')}` - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken - }) - })).json(); + identifier = 'reddit'; + name = 'Reddit'; + async refreshToken(refreshToken: string): Promise<AuthTokenDetails> { + const { + access_token: accessToken, + refresh_token: newRefreshToken, + expires_in: expiresIn, + } = await ( + await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + `${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}` + ).toString('base64')}`, + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }), + }) + ).json(); - const {name, id, icon_img} = await (await fetch('https://oauth.reddit.com/api/v1/me', { - headers: { - Authorization: `Bearer ${accessToken}` - } - })).json(); + const { name, id, icon_img } = await ( + await fetch('https://oauth.reddit.com/api/v1/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); - return { - id, - name, - accessToken, - refreshToken: newRefreshToken, - expiresIn, - picture: icon_img.split('?')[0] + return { + id, + name, + accessToken, + refreshToken: newRefreshToken, + expiresIn, + picture: icon_img.split('?')[0], + }; + } + + async generateAuthUrl() { + const state = makeId(6); + const codeVerifier = makeId(30); + const url = `https://www.reddit.com/api/v1/authorize?client_id=${ + process.env.REDDIT_CLIENT_ID + }&response_type=code&state=${state}&redirect_uri=${encodeURIComponent( + `${process.env.FRONTEND_URL}/integrations/social/reddit` + )}&duration=permanent&scope=${encodeURIComponent( + 'read identity submit flair' + )}`; + return { + url, + codeVerifier, + state, + }; + } + + async authenticate(params: { code: string; codeVerifier: string }) { + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + } = await ( + await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + `${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}` + ).toString('base64')}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: params.code, + redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/reddit`, + }), + }) + ).json(); + + const { name, id, icon_img } = await ( + await fetch('https://oauth.reddit.com/api/v1/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + ).json(); + + return { + id, + name, + accessToken, + refreshToken, + expiresIn, + picture: icon_img.split('?')[0], + }; + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[] + ): Promise<PostResponse[]> { + const [post, ...rest] = postDetails; + const response = await fetch('https://oauth.reddit.com/api/submit', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + title: 'test', + kind: 'self', + text: post.message, + sr: '/r/gitroom', + }), + }); + + return []; + } + + async subreddits(accessToken: string, data: any) { + const { + data: { children }, + } = await ( + await fetch( + `https://oauth.reddit.com/subreddits/search?show=public&q=${data.word}&sort=activity&show_users=false&limit=10`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, } + ) + ).json(); + + console.log(children); + return children.filter(({data} : {data: any}) => data.subreddit_type === "public").map(({ data: { title, url, id } }: any) => ({ + title, + name: url, + id, + })); + } + + private getPermissions(submissionType: string, allow_images: string) { + const permissions = []; + if (['any', 'self'].indexOf(submissionType) > -1) { + permissions.push('self'); } - async generateAuthUrl() { - const state = makeId(6); - const codeVerifier = makeId(30); - const url = `https://www.reddit.com/api/v1/authorize?client_id=${process.env.REDDIT_CLIENT_ID}&response_type=code&state=${state}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/integrations/social/reddit`)}&duration=permanent&scope=${encodeURIComponent('identity submit flair')}`; - return { - url, - codeVerifier, - state - } + if (['any', 'link'].indexOf(submissionType) > -1) { + permissions.push('link'); } - async authenticate(params: {code: string, codeVerifier: string}) { - const {access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn} = await (await fetch('https://www.reddit.com/api/v1/access_token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString('base64')}` - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code: params.code, - redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/reddit` - }) - })).json(); - - const {name, id, icon_img} = await (await fetch('https://oauth.reddit.com/api/v1/me', { - headers: { - Authorization: `Bearer ${accessToken}` - } - })).json(); - - return { - id, - name, - accessToken, - refreshToken, - expiresIn, - picture: icon_img.split('?')[0] - } + if (submissionType === "any" || allow_images) { + permissions.push('media'); } - async post(id: string, accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> { - const [post, ...rest] = postDetails; - const response = await fetch('https://oauth.reddit.com/api/submit', { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - title: 'test', - kind: 'self', - text: post.message, - sr: '/r/gitroom' - }) - }); + return permissions; + } - console.log(response); - return []; + async restrictions(accessToken: string, data: { subreddit: string }) { + const { + data: { submission_type, allow_images }, + } = await ( + await fetch(`https://oauth.reddit.com/${data.subreddit}/about`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + ).json(); + + const { + is_flair_required, + } = await ( + await fetch(`https://oauth.reddit.com/api/v1/${data.subreddit.split('/r/')[1]}/post_requirements`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + ).json(); + + const newData = await ( + await 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(); + + return { + subreddit: data.subreddit, + allow: this.getPermissions(submission_type, allow_images), + is_flair_required, + flairs: newData?.map?.((p: any) => ({ + id: p.id, + name: p.text + })) || [] } -} \ No newline at end of file + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index d49c6893..43da34d3 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -82,7 +82,6 @@ export class XProvider implements SocialProvider { accessToken: string, postDetails: PostDetails[], ): Promise<PostResponse[]> { - console.log('hello'); const client = new TwitterApi(accessToken); const {data: {username}} = await client.v2.me({ "user.fields": "username" diff --git a/libraries/react-shared-libraries/src/form/button.tsx b/libraries/react-shared-libraries/src/form/button.tsx index 38368c8f..c1c5f6c7 100644 --- a/libraries/react-shared-libraries/src/form/button.tsx +++ b/libraries/react-shared-libraries/src/form/button.tsx @@ -3,6 +3,6 @@ import {clsx} from "clsx"; export const Button: FC<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {secondary?: boolean}> = (props) => { return ( - <button {...props} type={props.type || 'button'} className={clsx(`${props.secondary ? 'bg-sixth' : 'bg-forth'} px-[24px] h-[40px] cursor-pointer items-center justify-center flex`, props?.className)} /> + <button {...props} type={props.type || 'button'} className={clsx(props.disabled && 'opacity-50 pointer-events-none' ,`${props.secondary ? 'bg-third' : 'bg-forth'} px-[24px] h-[40px] cursor-pointer items-center justify-center flex`, props?.className)} /> ) } \ No newline at end of file diff --git a/libraries/react-shared-libraries/src/form/input.tsx b/libraries/react-shared-libraries/src/form/input.tsx index 5b3e578c..22290f67 100644 --- a/libraries/react-shared-libraries/src/form/input.tsx +++ b/libraries/react-shared-libraries/src/form/input.tsx @@ -4,18 +4,19 @@ import {DetailedHTMLProps, FC, InputHTMLAttributes, useMemo} from "react"; import clsx from "clsx"; import {useFormContext} from "react-hook-form"; -export const Input: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {label: string, name: string}> = (props) => { - const {label, className, ...rest} = props; +export const Input: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => { + const {label, className, disableForm, error, ...rest} = props; const form = useFormContext(); const err = useMemo(() => { + if (error) return error; if (!form || !form.formState.errors[props?.name!]) return; return form?.formState?.errors?.[props?.name!]?.message! as string; - }, [form?.formState?.errors?.[props?.name!]?.message]); + }, [form?.formState?.errors?.[props?.name!]?.message, error]); return ( <div className="flex flex-col gap-[6px]"> <div className="font-['Inter'] text-[14px]">{label}</div> - <input {...form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} /> + <input {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} /> <div className="text-red-400 text-[12px]">{err || <> </>}</div> </div> ) diff --git a/libraries/react-shared-libraries/src/form/select.tsx b/libraries/react-shared-libraries/src/form/select.tsx index 6f61a9f3..8e8fc8fa 100644 --- a/libraries/react-shared-libraries/src/form/select.tsx +++ b/libraries/react-shared-libraries/src/form/select.tsx @@ -4,18 +4,19 @@ import {DetailedHTMLProps, FC, SelectHTMLAttributes, useMemo} from "react"; import {clsx} from "clsx"; import {useFormContext} from "react-hook-form"; -export const Select: FC<DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement> & {label: string, name: string}> = (props) => { - const {label, className, ...rest} = props; +export const Select: FC<DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => { + const {label, className, disableForm, error, ...rest} = props; const form = useFormContext(); const err = useMemo(() => { + if (error) return error; if (!form || !form.formState.errors[props?.name!]) return; return form?.formState?.errors?.[props?.name!]?.message! as string; - }, [form?.formState?.errors?.[props?.name!]?.message]); + }, [form?.formState?.errors?.[props?.name!]?.message, error]); return ( <div className="flex flex-col gap-[6px]"> <div className="font-['Inter'] text-[14px]">{label}</div> - <select {...form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} /> + <select {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} /> <div className="text-red-400 text-[12px]">{err || <> </>}</div> </div> ) diff --git a/libraries/react-shared-libraries/src/form/textarea.tsx b/libraries/react-shared-libraries/src/form/textarea.tsx new file mode 100644 index 00000000..588922b3 --- /dev/null +++ b/libraries/react-shared-libraries/src/form/textarea.tsx @@ -0,0 +1,24 @@ +"use client"; + +import {DetailedHTMLProps, FC, InputHTMLAttributes, useMemo} from "react"; +// @ts-ignore +import clsx from "clsx"; +import {useFormContext} from "react-hook-form"; + +export const Textarea: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => { + const {label, className, disableForm, error, ...rest} = props; + const form = useFormContext(); + const err = useMemo(() => { + if (error) return error; + if (!form || !form.formState.errors[props?.name!]) return; + return form?.formState?.errors?.[props?.name!]?.message! as string; + }, [form?.formState?.errors?.[props?.name!]?.message, error]); + + return ( + <div className="flex flex-col gap-[6px]"> + <div className="font-['Inter'] text-[14px]">{label}</div> + <textarea {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input min-h-[150px] p-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} /> + <div className="text-red-400 text-[12px]">{err || <> </>}</div> + </div> + ) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bd101d12..76ef029f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "cookie-parser": "^1.4.6", "dayjs": "^1.11.10", "ioredis": "^5.3.2", + "json-to-graphql-query": "^2.2.5", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "md5": "^2.3.0", @@ -60,6 +61,7 @@ "react-router-dom": "6.11.2", "react-tag-autocomplete": "^7.2.0", "react-tooltip": "^5.26.2", + "react-use-keypress": "^1.3.1", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", @@ -70,6 +72,7 @@ "swr": "^2.2.5", "tslib": "^2.3.0", "twitter-api-v2": "^1.16.0", + "use-debounce": "^10.0.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -15954,6 +15957,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-to-graphql-query": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz", + "integrity": "sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -20045,6 +20053,17 @@ "react-dom": ">=16.14.0" } }, + "node_modules/react-use-keypress": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-use-keypress/-/react-use-keypress-1.3.1.tgz", + "integrity": "sha512-fo+LQrxviMcZt7efCFPc6CX9/oNEPD+MJ/qSs4nK3/lyRNtquhG9f1J8GQq2VFfIYUVDUdPKz8fGIwErO1Pcuw==", + "dependencies": { + "tiny-invariant": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17 || 18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -22430,6 +22449,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.2.tgz", + "integrity": "sha512-oLXoWt7bk7SI3REp16Hesm0kTBTErhk+FWTvuujYMlIbX42bb3yLN98T3OyzFNkZ3WAjVYDL4sWykCR6kD2mqQ==" + }, "node_modules/tinybench": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", @@ -23257,6 +23281,17 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/use-debounce": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz", + "integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", diff --git a/package.json b/package.json index 69d5232f..5361ba85 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "cookie-parser": "^1.4.6", "dayjs": "^1.11.10", "ioredis": "^5.3.2", + "json-to-graphql-query": "^2.2.5", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "md5": "^2.3.0", @@ -60,6 +61,7 @@ "react-router-dom": "6.11.2", "react-tag-autocomplete": "^7.2.0", "react-tooltip": "^5.26.2", + "react-use-keypress": "^1.3.1", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", @@ -70,6 +72,7 @@ "swr": "^2.2.5", "tslib": "^2.3.0", "twitter-api-v2": "^1.16.0", + "use-debounce": "^10.0.0", "yargs": "^17.7.2" }, "devDependencies": {