├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── Card │ └── Card.tsx ├── Editor │ └── useTipTap.tsx ├── Layout │ ├── LayouDashboard.tsx │ └── LayoutPublic.tsx ├── Pagination │ └── Paging.tsx └── SideNav │ ├── Desktop.tsx │ └── Mobile.tsx ├── hooks └── post │ ├── create.ts │ ├── get.ts │ ├── remove.ts │ └── update.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── admin │ ├── dashboard.tsx │ ├── login.tsx │ └── post │ │ ├── [slug].tsx │ │ └── create.tsx ├── api │ ├── admin │ │ └── post │ │ │ ├── [slug].ts │ │ │ ├── create.ts │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ └── update.ts │ └── auth │ │ └── [...nextauth].ts ├── index.tsx └── post │ └── [slug].tsx ├── public ├── favicon.ico ├── github.png └── vercel.svg ├── styles ├── Home.module.css └── globals.css ├── tsconfig.json ├── types └── types.ts └── utils ├── GithubAPI.ts ├── fetcher.ts └── parseContentFromGithub.ts /.env.local.example: -------------------------------------------------------------------------------- 1 | GITHUB_KEY= YOUR GITHUB PERSONAL TOKEN 2 | OAUTH_CLIENT= GITHUB CLIENT ID 3 | OAUTH_SECRET= GITHUB CLIENT SECRET 4 | REPO_NAME= REPO NAME FOR STORING CONTENT 5 | 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/52363719/182028441-acfe1a67-53c3-4503-8ab3-2a84f21fba96.png) 2 | # Git Blog 3 | Just a simple blogging website using github repository for storing post content 4 | created for purpose learning SWR and NextJS API 5 | 6 | Tech Stack used 7 | - [NextJS]([https://www.markdownguide.org](https://nextjs.org/)) 8 | - [SWR](https://swr.vercel.app/) 9 | - [Chakra UI](https://chakra-ui.com/) 10 | - [Next Auth](https://next-auth.js.org/) 11 | - [Octokit](https://octokit.github.io/rest.js/) 12 | - [TipTap WYSIWYG](https://tiptap.dev/) 13 | 14 | ## Route List (Main) 15 | - / 16 | - /admin/dashboard 17 | - /admin/login 18 | 19 | ## How to use it 20 | - As always start with npm install 21 | - Create your github personal token, you can read it [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 22 | - Create Github OAuth App, you can read it [here](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps) 23 | - Add everything on env 24 | - Don't worry you dont have to create content repository manually, it's automatically created while you created first post 25 | 26 | ## Upcoming Feature 27 | - [ ] Post Comment 28 | - [ ] Thumbnail Post 29 | - [ ] Post Asset with image 30 | 31 | ## Screenshoot 32 | ![image](https://user-images.githubusercontent.com/52363719/182028773-b13302d2-39fe-4214-a4f9-54440392fd6c.png) 33 | ![image](https://user-images.githubusercontent.com/52363719/182028777-e1c3d3ef-588e-41ca-a689-c99c907f0322.png) 34 | ![image](https://user-images.githubusercontent.com/52363719/182028791-85430b48-7bb4-4bbf-b55d-a430824c6bd0.png) 35 | ![image](https://user-images.githubusercontent.com/52363719/182028807-4b5bb981-cea8-4696-ab53-8de609cf6273.png) 36 | ![image](https://user-images.githubusercontent.com/52363719/182028818-c24017d8-fdc7-4dd3-89b1-fd11cb8f194d.png) 37 | ![image](https://user-images.githubusercontent.com/52363719/182028822-f9f3c962-bcd0-446b-9c05-11eab8848bac.png) 38 | ![image](https://user-images.githubusercontent.com/52363719/182029001-841d5d4f-ebce-43f0-a4d1-c745d0ca1e1e.png) 39 | 40 | 41 | -------------------------------------------------------------------------------- /components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex, Heading } from "@chakra-ui/react"; 3 | 4 | type Props = { 5 | slug: string; 6 | title: string; 7 | }; 8 | 9 | export default function Card({ ...props }: Props) { 10 | return ( 11 | 27 | 28 | {props.title} 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/Editor/useTipTap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Box, Button, Divider, Flex, FormLabel } from "@chakra-ui/react"; 3 | 4 | import { useEditor, EditorContent, HTMLContent } from "@tiptap/react"; 5 | import StarterKit from "@tiptap/starter-kit"; 6 | 7 | type EDITOR_STYLE = { 8 | toolbar?: boolean; 9 | border?: boolean; 10 | }; 11 | 12 | export default function useTipTap( 13 | content: string = "", 14 | editable: boolean = true 15 | ): [ 16 | ({ toolbar, border }: EDITOR_STYLE) => JSX.Element, 17 | HTMLContent | undefined 18 | ] { 19 | const editor = useEditor({ 20 | extensions: [StarterKit], 21 | content: content, 22 | editable: editable, 23 | }); 24 | 25 | useEffect(() => { 26 | if (content !== "") { 27 | editor?.commands.setContent(content); 28 | } 29 | }, [content]); 30 | 31 | const Element = React.useMemo(() => { 32 | const Editor = ({ toolbar = true, border = true }: EDITOR_STYLE) => ( 33 | 42 | 43 | {toolbar && Content} 44 | 45 | 46 | {toolbar && ( 47 | <> 48 | 49 | 59 | 69 | 79 | 91 | 103 | 115 | 129 | 143 | 157 | 171 | 183 | 184 | 185 | 186 | )} 187 | 188 | 189 | 190 | 191 | 192 | ); 193 | 194 | return Editor; 195 | }, [editor]); 196 | 197 | return [Element, editor?.getHTML()]; 198 | } 199 | -------------------------------------------------------------------------------- /components/Layout/LayouDashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Box, Grid, GridItem } from "@chakra-ui/react"; 3 | 4 | import SideDesktop from "../SideNav/Desktop"; 5 | import SideMobile from "../SideNav/Mobile"; 6 | import { useSession } from "next-auth/react"; 7 | import { useRouter } from "next/router"; 8 | 9 | type Props = { 10 | children: React.ReactNode; 11 | }; 12 | 13 | export default function LayouDashboard({ ...props }: Props) { 14 | const { data, status } = useSession(); 15 | const loading = status === "loading"; 16 | const router = useRouter(); 17 | 18 | useEffect(() => { 19 | if (!loading) { 20 | if (!data) { 21 | router.push("/admin/login?error=true"); 22 | } 23 | } 24 | }, [loading]); 25 | 26 | if (typeof window !== undefined && loading) { 27 | return null; 28 | } 29 | 30 | return ( 31 | 32 | 33 | 39 | 40 | 41 | {props.children} 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /components/Layout/LayoutPublic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex, Heading, Input } from "@chakra-ui/react"; 3 | import NextLink from "next/link"; 4 | 5 | type Props = { 6 | children: React.ReactNode; 7 | searchProps?:{ 8 | change:(e:any)=>void; 9 | value:string; 10 | }, 11 | displaySearch?:boolean; 12 | }; 13 | export default function LayoutPublic({ children,searchProps={ 14 | value:"", 15 | change:()=>{} 16 | },displaySearch=false }: Props) { 17 | return ( 18 | <> 19 | 20 | 21 | 22 | Git-Blog 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Pagination/Paging.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Button, Flex } from "@chakra-ui/react"; 3 | 4 | type Props = { 5 | data: any[]; 6 | }; 7 | 8 | export default function usePaging(): [ 9 | ({ data }: Props) => JSX.Element, 10 | { 11 | length: number; 12 | active: number; 13 | } 14 | ] { 15 | const [paging, setpaging] = useState({ 16 | length: 6, 17 | active: 0, 18 | }); 19 | const Element = React.useMemo(() => { 20 | const PageList = ({ data }: Props) => ( 21 | 22 | {Array.from(Array(Math.round(data.length / paging.length)).keys()).map( 23 | (el) => ( 24 | 33 | ) 34 | )} 35 | 36 | ); 37 | 38 | return PageList; 39 | }, [paging]); 40 | return [Element, paging]; 41 | } 42 | -------------------------------------------------------------------------------- /components/SideNav/Desktop.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Flex, 5 | GridItem, 6 | Heading, 7 | Stack, 8 | Text, 9 | Button, 10 | Divider, 11 | } from "@chakra-ui/react"; 12 | 13 | import { Avatar} from "@chakra-ui/react"; 14 | import { signOut, useSession } from "next-auth/react"; 15 | 16 | export default function Desktop() { 17 | const {data} =useSession() 18 | 19 | return ( 20 | 27 | Git-Blog 28 | 29 | Welcome Back 30 | 31 | 32 | 33 | Aqshola 34 | 35 | 36 | 37 | 38 | 39 | 48 | 49 | 50 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/SideNav/Mobile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { HamburgerIcon } from "@chakra-ui/icons"; 3 | import { 4 | Box, 5 | Flex, 6 | Heading, 7 | Stack, 8 | Text, 9 | Button, 10 | Divider, 11 | Slide, 12 | } from "@chakra-ui/react"; 13 | 14 | import { Avatar } from "@chakra-ui/react"; 15 | import { signOut, useSession } from "next-auth/react"; 16 | 17 | export default function Mobile() { 18 | const [show, setshow] = useState(false); 19 | const { data } = useSession(); 20 | 21 | function _handleShow(value: boolean) { 22 | setshow(value); 23 | if (document) { 24 | if (value) { 25 | document.body.style.overflow = "hidden"; 26 | } else { 27 | document.body.style.overflow = "auto"; 28 | } 29 | } 30 | } 31 | 32 | return ( 33 | <> 34 | 47 | 48 | 56 | 68 | Git-Blog 69 | 70 | Welcome Back 71 | 72 | 76 | 77 | Aqshola 78 | 79 | 80 | 81 | 82 | 83 | 92 | 93 | 94 | 102 | 103 | 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /hooks/post/create.ts: -------------------------------------------------------------------------------- 1 | import { HTMLContent } from "@tiptap/react"; 2 | import React, { useState } from "react"; 3 | import { RESPONSE_POST } from "types/types"; 4 | 5 | export default function useCreate(): [ 6 | (title: string, content: HTMLContent,callback:()=>any) => Promise, 7 | "success" | "failed" | null, 8 | string | null, 9 | boolean 10 | ] { 11 | const [loading, setloading] = useState(false); 12 | const [error, seterror] = useState(null); 13 | const [status, setstatus] = useState<"success" | "failed" | null>(null); 14 | return [ 15 | async (title: string, content: HTMLContent,callback?:()=>any) => { 16 | setloading(true); 17 | const res = await fetch("/api/admin/post/create", { 18 | method: "POST", 19 | headers: { 20 | "Content-Type": "application/json", 21 | }, 22 | body: JSON.stringify({ 23 | title: title, 24 | content, 25 | }), 26 | }); 27 | const data = (await res.json()) as RESPONSE_POST; 28 | if(callback){ 29 | callback() 30 | } 31 | setstatus("success"); 32 | if (data.status === "failed") { 33 | seterror(data.errorMsg); 34 | } 35 | setloading(false); 36 | return; 37 | }, 38 | status, 39 | error, 40 | loading, 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /hooks/post/get.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function get() { 4 | return [] 5 | } 6 | -------------------------------------------------------------------------------- /hooks/post/remove.ts: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RESPONSE_POST } from "types/types"; 3 | 4 | export default function useRemove(): [ 5 | (title: string, callback: () => any) => Promise, 6 | "success" | "failed" | null, 7 | string | null, 8 | boolean 9 | ] { 10 | const [loading, setloading] = useState(false); 11 | const [error, seterror] = useState(null); 12 | const [status, setstatus] = useState<"success" | "failed" | null>(null); 13 | return [ 14 | async (title: string, callback: () => any) => { 15 | setloading(true); 16 | const res = await fetch("/api/admin/post/delete", { 17 | method: "DELETE", 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify({ 22 | title: title, 23 | }), 24 | }); 25 | const data = (await res.json()) as RESPONSE_POST; 26 | setstatus(data.status); 27 | if (data.errorMsg) { 28 | seterror(data.errorMsg); 29 | } 30 | if (callback) { 31 | callback(); 32 | } 33 | 34 | setloading(false); 35 | return; 36 | }, 37 | status, 38 | error, 39 | loading, 40 | ]; 41 | } 42 | -------------------------------------------------------------------------------- /hooks/post/update.ts: -------------------------------------------------------------------------------- 1 | import { HTMLContent } from "@tiptap/react"; 2 | import React, { useState } from "react"; 3 | import { POST_TYPE, RESPONSE_POST } from "types/types"; 4 | 5 | export default function useUpdate():[(title:string, content:HTMLContent, callback?:()=>any)=>Promise, "success" | "failed" | null, string | null, boolean] { 6 | const [loading, setloading] = useState(false); 7 | const [status, setstatus] = useState<"success" | "failed" | null>(null); 8 | const [error, seterror] = useState(null); 9 | 10 | return [ 11 | async (title: string, content: HTMLContent, callback?: () => any) => { 12 | setloading(true); 13 | const res = await fetch("/api/admin/post/update", { 14 | method: "PUT", 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | body: JSON.stringify({ 19 | title: title, 20 | content, 21 | }), 22 | }); 23 | const data = (await res.json()) as RESPONSE_POST; 24 | setstatus(data.status); 25 | if (data.errorMsg) { 26 | seterror(data.errorMsg); 27 | } 28 | if (callback) { 29 | callback(); 30 | } 31 | 32 | setloading(false); 33 | return; 34 | }, 35 | status, 36 | error, 37 | loading, 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/icons": "^2.0.4", 13 | "@chakra-ui/react": "^2.2.4", 14 | "@emotion/react": "^11", 15 | "@emotion/styled": "^11", 16 | "@tiptap/react": "^2.0.0-beta.114", 17 | "@tiptap/starter-kit": "^2.0.0-beta.191", 18 | "framer-motion": "^6", 19 | "next": "12.2.2", 20 | "next-auth": "^4.10.2", 21 | "octokit": "^2.0.4", 22 | "octokit-plugin-create-pull-request": "^3.12.2", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "swr": "^1.3.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "18.0.3", 29 | "@types/react": "18.0.15", 30 | "@types/react-dom": "18.0.6", 31 | "eslint": "8.19.0", 32 | "eslint-config-next": "12.2.2", 33 | "typescript": "4.7.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | import { SessionProvider } from "next-auth/react"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default MyApp; 17 | -------------------------------------------------------------------------------- /pages/admin/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, Stack, Button, Spinner, Box } from "@chakra-ui/react"; 2 | 3 | import React, { useState } from "react"; 4 | import Card from "../../components/Card/Card"; 5 | import LayouDashboard from "../../components/Layout/LayouDashboard"; 6 | import NextLink from "next/link"; 7 | import useSWR from "swr"; 8 | import fetcher from "utils/fetcher"; 9 | import usePaging from "components/Pagination/Paging"; 10 | import NextHead from "next/head" 11 | 12 | 13 | 14 | export default function Dashboard() { 15 | const { data } = useSWR("/api/admin/post/get", fetcher); 16 | const [Paging, dataPaging] = usePaging(); 17 | 18 | return ( 19 | 20 | 21 | Dashboard1 22 | 23 | Post 24 | 25 | 28 | 29 | 30 | {!data && ( 31 | 32 | 33 | 34 | )} 35 | 36 | {data && data.data.length == 0 && ( 37 | 38 | No Post Yet 😟 39 | 40 | )} 41 | 42 | {data && 43 | data.data.length > 0 && 44 | data.data 45 | .slice(dataPaging.active, dataPaging.active + dataPaging.length) 46 | .map((el: any, i: number) => ( 47 | 48 | 49 | 50 | 51 | 52 | ))} 53 | 54 | 55 | {data && } 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /pages/admin/login.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Box, 4 | Button, 5 | AlertDescription, 6 | AlertTitle, 7 | AlertIcon, 8 | CloseButton, 9 | Stack, 10 | useDisclosure, 11 | } from "@chakra-ui/react"; 12 | import React, { useEffect } from "react"; 13 | import NextImage from "next/image"; 14 | import fetcher from "utils/fetcher"; 15 | import useSWR from "swr"; 16 | import { signIn, useSession } from "next-auth/react"; 17 | import { useRouter } from "next/router"; 18 | export default function Login() { 19 | const router = useRouter(); 20 | const { error } = router.query; 21 | const { isOpen, onClose, onOpen } = useDisclosure({ defaultIsOpen: false }); 22 | const { data, status } = useSession(); 23 | const loading = status === "loading"; 24 | 25 | useEffect(() => { 26 | if (error) { 27 | onOpen(); 28 | } 29 | }, [error]); 30 | 31 | useEffect(() => { 32 | if (!loading) { 33 | if (data) { 34 | router.push("/admin/dashboard"); 35 | } 36 | } 37 | }, [loading]); 38 | 39 | if (typeof window !== undefined && loading) { 40 | return null; 41 | } 42 | 43 | 44 | 45 | return ( 46 | 56 | {isOpen && ( 57 | 58 | 59 | 60 | 61 | You didnt have permission to access 62 | 63 | 64 | 65 | 66 | )} 67 | 68 | 74 | 75 | 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /pages/admin/post/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Flex, 5 | FormLabel, 6 | Heading, 7 | Input, 8 | Spinner, 9 | } from "@chakra-ui/react"; 10 | import React, { useEffect, useState } from "react"; 11 | import useTipTap from "components/Editor/useTipTap"; 12 | import LayouDashboard from "components/Layout/LayouDashboard"; 13 | import useSwr, { useSWRConfig } from "swr"; 14 | import { useToast } from "@chakra-ui/react"; 15 | import { useRouter } from "next/router"; 16 | import NextLink from "next/link"; 17 | import { POST_TYPE, RESPONSE_POST } from "types/types"; 18 | import useUpdate from "hooks/post/update"; 19 | import useRemove from "hooks/post/remove"; 20 | import fetcher from "utils/fetcher"; 21 | import NextHead from "next/head"; 22 | 23 | export default function UpdatePost() { 24 | const [title, settitle] = useState(""); 25 | const toast = useToast(); 26 | const router = useRouter(); 27 | const { slug } = router.query; 28 | 29 | const { data: dataPost, error: errorPost } = useSwr>( 30 | slug ? `/api/admin/post/${slug}` : null, 31 | fetcher 32 | ); 33 | const { mutate } = useSWRConfig(); 34 | 35 | const [Editor, content] = useTipTap(dataPost?.data.content); 36 | const [update, statusUpdate, errorUpdate, loadingUpdate] = useUpdate(); 37 | const [remove, statusRemove, errorRemove, loadingRemove] = useRemove(); 38 | 39 | useEffect(() => { 40 | if (!loadingUpdate && statusUpdate != null) { 41 | if (statusUpdate === "success") { 42 | router.push("/admin/dashboard"); 43 | toast({ 44 | title: `Post ${title} Updated`, 45 | isClosable: true, 46 | status: "success", 47 | duration: 5000, 48 | }); 49 | } else { 50 | toast({ 51 | title: `Post ${title} Failed to Removed`, 52 | isClosable: true, 53 | status: "error", 54 | duration: 5000, 55 | }); 56 | } 57 | } 58 | }, [loadingUpdate]); 59 | 60 | useEffect(() => { 61 | if (!loadingRemove && statusRemove != null) { 62 | if (statusRemove === "success") { 63 | router.push("/admin/dashboard"); 64 | toast({ 65 | title: `Post ${title} Removed`, 66 | isClosable: true, 67 | status: "success", 68 | duration: 5000, 69 | }); 70 | } else { 71 | toast({ 72 | title: `Post ${title} Failed to Removed`, 73 | isClosable: true, 74 | status: "error", 75 | duration: 5000, 76 | }); 77 | } 78 | } 79 | }, [loadingRemove]); 80 | 81 | async function _submit(titleParam: string) { 82 | await update(titleParam, content || "", () => { 83 | mutate(`/api/admin/post/${slug}`); 84 | }); 85 | } 86 | 87 | async function _delete(titleParam: string) { 88 | remove(titleParam, () => mutate("/admin/dashboard")); 89 | } 90 | 91 | return ( 92 | 93 | Update Post 94 | 95 | {!dataPost && ( 96 | 97 | 98 | 99 | )} 100 | 101 | {dataPost && dataPost.data && ( 102 | <> 103 | 104 | Edit {dataPost.data.title} 105 | 106 |
{ 108 | e.preventDefault(); 109 | _submit(dataPost.data.title); 110 | }} 111 | > 112 | 113 | 114 | Title 115 | settitle(e.target.value)} 119 | required 120 | id="title" 121 | placeholder="Title" 122 | size={"lg"} 123 | /> 124 | 125 | 126 | 127 | 128 | 131 | 132 | 141 | 149 | 150 | 151 |
152 | 153 | )} 154 |
155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /pages/admin/post/create.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Flex, FormLabel, Heading, Input } from "@chakra-ui/react"; 2 | import React, { useEffect, useState } from "react"; 3 | import useTipTap from "components/Editor/useTipTap"; 4 | import LayouDashboard from "components/Layout/LayouDashboard"; 5 | import { useSWRConfig } from "swr"; 6 | import { useToast } from "@chakra-ui/react"; 7 | import { useRouter } from "next/router"; 8 | import useCreate from "hooks/post/create"; 9 | 10 | type Props = {}; 11 | 12 | export default function Create({}: Props) { 13 | const [Editor, content] = useTipTap(); 14 | const [title, settitle] = useState(""); 15 | const toast = useToast(); 16 | const router = useRouter(); 17 | const { mutate } = useSWRConfig(); 18 | const [create, status, error, loading] = useCreate(); 19 | 20 | useEffect(() => { 21 | if (!loading && status!== null) { 22 | if( status === "success"){ 23 | 24 | router.push("/admin/dashboard"); 25 | toast({ 26 | title: "Post Created Successfuly", 27 | isClosable: true, 28 | status: "success", 29 | duration: 5000, 30 | }); 31 | }else{ 32 | toast({ 33 | title: "Failed Creating Post", 34 | isClosable: true, 35 | status: "error", 36 | duration: 5000, 37 | }); 38 | } 39 | } 40 | }, [loading]); 41 | 42 | async function _submit() { 43 | create(title, content || "", () => mutate("/admin/dashboard")); 44 | } 45 | 46 | return ( 47 | 48 | Create Post 49 | 50 |
{ 52 | e.preventDefault(); 53 | _submit(); 54 | }} 55 | > 56 | 57 | 58 | Title 59 | settitle(e.target.value)} 61 | required 62 | id="title" 63 | placeholder="Title" 64 | size={"lg"} 65 | /> 66 | 67 | 68 | 69 | 77 | 78 | 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /pages/api/admin/post/[slug].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import GithubAPI from "utils/GithubAPI"; 4 | 5 | import {POST_TYPE, RESPONSE_POST} from "types/types" 6 | import parseContentFromGithub from "utils/parseContentFromGithub"; 7 | 8 | 9 | export default async function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse> 12 | ) { 13 | const { method} = req; 14 | 15 | if (method !== "GET") { 16 | res 17 | .status(405) 18 | .send({ data:null,status: "failed", errorMsg: "Only GET requests allowed" }); 19 | return; 20 | } 21 | 22 | const { slug } = req.query; 23 | const github =GithubAPI() 24 | 25 | try { 26 | const username = await github.rest.users.getAuthenticated(); 27 | 28 | const indexJson = await github.rest.repos.getContent({ 29 | owner: username.data.login, 30 | path: `index.json`, 31 | repo: process.env.REPO_NAME||"", 32 | }); 33 | 34 | let JsonData = parseContentFromGithub(indexJson.data); 35 | 36 | if (JsonData) { 37 | JsonData = JSON.parse(JsonData) as Array; 38 | const indexPost = JsonData.find((post: any) => post.slug === slug); 39 | 40 | if (indexPost) { 41 | const postDetailJson = await github.rest.repos.getContent({ 42 | owner: username.data.login, 43 | path: indexPost.path, 44 | repo: process.env.REPO_NAME||"", 45 | }); 46 | const htmlContent = parseContentFromGithub(postDetailJson.data); 47 | res.status(200).send({ 48 | data: { 49 | path: indexPost.path, 50 | title:indexPost.title, 51 | slug:indexPost.slug, 52 | createAt:indexPost.createAt, 53 | content: htmlContent, 54 | }, 55 | status: "success", 56 | }); 57 | return; 58 | } else { 59 | res 60 | .status(404) 61 | .send({ data:null,status: "failed", errorMsg: "POST NOT INDEXED" }); 62 | return; 63 | } 64 | } 65 | } catch (error) { 66 | res.status(500).send({ data:null,status: "failed", errorMsg: error }); 67 | return; 68 | } 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /pages/api/admin/post/create.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { HTMLContent } from "@tiptap/react"; 4 | import GithubAPI from "utils/GithubAPI"; 5 | import { POST_TYPE, RESPONSE_POST } from "types/types"; 6 | import parseContentFromGithub from "utils/parseContentFromGithub"; 7 | 8 | type BodyReq = { 9 | title: string; 10 | content: HTMLContent; 11 | }; 12 | 13 | export default async function handler( 14 | req: NextApiRequest, 15 | res: NextApiResponse> 16 | ) { 17 | const { method, body } = req; 18 | const github = GithubAPI(); 19 | 20 | if (method !== "POST") { 21 | res.status(405).send({ 22 | data: null, 23 | status: "failed", 24 | errorMsg: "Only POST requests allowed", 25 | }); 26 | return; 27 | } 28 | 29 | const { title, content } = body as BodyReq; 30 | 31 | try { 32 | const slug = title.replaceAll(" ", "-").toLowerCase(); 33 | const username = await github.rest.users.getAuthenticated(); 34 | 35 | try { 36 | await github.rest.repos.get({ 37 | owner: username.data.login, 38 | repo: process.env.REPO_NAME || "", 39 | }); 40 | } catch (error: any) { 41 | if (error.status === 404) { 42 | await github.rest.repos.createForAuthenticatedUser({ 43 | name: process.env.REPO_NAME || "", 44 | private: true, 45 | }); 46 | github.request("PUT /repos/{owner}/{repo}/contents/{path}", { 47 | owner: "Aqshola", 48 | repo: process.env.REPO_NAME || "", 49 | path: "index.json", 50 | message: "test push", 51 | content: Buffer.from(JSON.stringify([])).toString("base64"), 52 | }); 53 | } 54 | } 55 | 56 | try { 57 | const checkPostExist = await github.rest.repos.getContent({ 58 | owner: username.data.login, 59 | path: `post/${slug}`, 60 | repo: process.env.REPO_NAME || "", 61 | }); 62 | 63 | if (checkPostExist.data) { 64 | res.status(409).json({ 65 | data: null, 66 | status: "failed", 67 | errorMsg: "Post already exist", 68 | }); 69 | return; 70 | } 71 | } catch (error: any) {} 72 | 73 | //CREATE POST 74 | try { 75 | const indexJson = await github.rest.repos.getContent({ 76 | owner: username.data.login, 77 | path: `index.json`, 78 | repo: process.env.REPO_NAME || "", 79 | }); 80 | 81 | let JsonData = parseContentFromGithub(indexJson.data); 82 | if (JsonData) { 83 | JsonData = JSON.parse(JsonData) as Array; 84 | const objectPost = { 85 | title: title, 86 | path: `post/${slug}/content.html`, 87 | createAt: new Date(), 88 | slug: slug, 89 | }; 90 | 91 | JsonData.unshift(objectPost); 92 | 93 | const pr = await github.createPullRequest({ 94 | owner: username.data.login, 95 | repo: process.env.REPO_NAME || "", 96 | title: `Create new post ${title}`, 97 | body: "", 98 | head: slug, 99 | createWhenEmpty: true, 100 | 101 | base: "main" /* optional: defaults to default branch */, 102 | update: 103 | false /* optional: set to `true` to enable updating existing pull requests */, 104 | forceFork: 105 | false /* optional: force creating fork even when user has write rights */, 106 | changes: [ 107 | { 108 | /* optional: if `files` is not passed, an empty commit is created instead */ 109 | files: { 110 | [`post/${slug}/content.html`]: content, 111 | [`index.json`]: JSON.stringify(JsonData), 112 | }, 113 | commit: "NEW POST", 114 | }, 115 | ], 116 | }); 117 | 118 | if (pr) { 119 | await github.rest.pulls.merge({ 120 | owner: username.data.login, 121 | repo: process.env.REPO_NAME || "", 122 | pull_number: pr?.data.number, 123 | }); 124 | return res 125 | .status(200) 126 | .send({ data: { ...objectPost, content }, status: "success" }); 127 | } else { 128 | console.log("errr"); 129 | return res 130 | .status(500) 131 | .send({ data: null, status: "failed", errorMsg: "API ERROR" }); 132 | } 133 | } 134 | } catch (error) { 135 | res.status(500).send({ data: null, status: "failed", errorMsg: error }); 136 | } 137 | } catch (error) { 138 | res.status(500).send({ data: null, status: "failed", errorMsg: error }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /pages/api/admin/post/delete.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { Octokit,} from "octokit"; 4 | import { HTMLContent,} from "@tiptap/react"; 5 | import { createPullRequest } from "octokit-plugin-create-pull-request"; 6 | import { RESPONSE_POST } from "types/types"; 7 | import GithubAPI from "utils/GithubAPI"; 8 | import parseContentFromGithub from "utils/parseContentFromGithub"; 9 | 10 | 11 | 12 | type BodyReq = { 13 | title: string; 14 | content: HTMLContent; 15 | }; 16 | 17 | export default async function handler( 18 | req: NextApiRequest, 19 | res: NextApiResponse> 20 | ) { 21 | const { method, body } = req; 22 | const github=GithubAPI() 23 | 24 | if (method !== "DELETE") { 25 | res 26 | .status(405) 27 | .send({ data:null,status: "failed", errorMsg: "Only DELETE requests allowed" }); 28 | return; 29 | } 30 | 31 | const { title} = body as BodyReq; 32 | 33 | try { 34 | const slug = title.replaceAll(" ", "-").toLowerCase(); 35 | const username = await github.rest.users.getAuthenticated(); 36 | 37 | const indexJson = await github.rest.repos.getContent({ 38 | owner: username.data.login, 39 | path: `index.json`, 40 | repo: process.env.REPO_NAME||"", 41 | }); 42 | 43 | let JsonData = parseContentFromGithub(indexJson.data); 44 | 45 | if (JsonData) { 46 | JsonData = JSON.parse(JsonData) as Array; 47 | const indexPost = JsonData.filter((post: any) => post.slug !== slug); 48 | 49 | if (indexPost) { 50 | const checkPostExist = await github.rest.repos.getContent({ 51 | owner: username.data.login, 52 | path: `post/${slug}`, 53 | repo: process.env.REPO_NAME||"", 54 | }); 55 | 56 | if (checkPostExist.data) { 57 | const pr = await github.createPullRequest({ 58 | owner: username.data.login, 59 | repo: process.env.REPO_NAME||"", 60 | title: `Delete new post ${title}`, 61 | body: "", 62 | head: slug, 63 | createWhenEmpty: true, 64 | forceFork: 65 | false /* optional: force creating fork even when user has write rights */, 66 | changes: [ 67 | { 68 | /* optional: if `files` is not passed, an empty commit is created instead */ 69 | files: { 70 | [`post/${slug}/content.html`]: null, 71 | [`index.json`]: JSON.stringify(indexPost), 72 | }, 73 | 74 | commit: "DELETE", 75 | }, 76 | ], 77 | }); 78 | 79 | if (pr) { 80 | await github.rest.pulls.merge({ 81 | owner: username.data.login, 82 | repo: process.env.REPO_NAME||"", 83 | pull_number: pr?.data.number, 84 | }); 85 | 86 | return res.status(200).send({ data:null,status: "success" }); 87 | 88 | } 89 | } 90 | } else { 91 | return res 92 | .status(404) 93 | .send({ data:null,status: "failed", errorMsg: "POST NOT INDEXED" }); 94 | 95 | } 96 | } 97 | } catch (error) { 98 | 99 | return res.status(500).send({ data:null, status: "failed", errorMsg: error }); 100 | 101 | } 102 | } 103 | 104 | 105 | -------------------------------------------------------------------------------- /pages/api/admin/post/get.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import GithubAPI from "utils/GithubAPI"; 4 | import { POST_TYPE, RESPONSE_POST } from "types/types"; 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse> 9 | ) { 10 | const { method, body } = req; 11 | const github = GithubAPI(); 12 | 13 | if (method !== "GET") { 14 | res.status(405).send({ 15 | data: [], 16 | status: "failed", 17 | errorMsg: "Only GET requests allowed", 18 | }); 19 | return; 20 | } 21 | 22 | const username = await github.rest.users.getAuthenticated(); 23 | 24 | try { 25 | const listPost = await github.rest.repos.getContent({ 26 | owner: username.data.login, 27 | path: `index.json`, 28 | repo: process.env.REPO_NAME || "", 29 | }); 30 | let parsedListPost = listPost.data as any; 31 | 32 | let dataListPost = JSON.parse( 33 | Buffer.from(parsedListPost.content, "base64").toString() 34 | ) as Array; 35 | 36 | return res.status(200).json({ 37 | data: dataListPost, 38 | status: "success", 39 | }); 40 | } catch (error: any) { 41 | res.status(error.status).json({ 42 | data: [], 43 | status: "failed", 44 | errorMsg: "API Error", 45 | }); 46 | return; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pages/api/admin/post/update.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { HTMLContent } from "@tiptap/react"; 4 | import GithubAPI from "utils/GithubAPI"; 5 | import { POST_TYPE, RESPONSE_POST } from "types/types"; 6 | 7 | type BodyReq = { 8 | title: string; 9 | content: HTMLContent; 10 | }; 11 | 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse> 15 | ) { 16 | const { method, body } = req; 17 | const github = GithubAPI(); 18 | 19 | if (method !== "PUT") { 20 | res 21 | .status(405) 22 | .send({data:null, status: "failed", errorMsg: "Only PUT requests allowed" }); 23 | return; 24 | } 25 | 26 | const { title, content } = body as BodyReq; 27 | 28 | try { 29 | const slug = title.replaceAll(" ", "-").toLowerCase(); 30 | const username = await github.rest.users.getAuthenticated(); 31 | 32 | try { 33 | const checkPostExist = await github.rest.repos.getContent({ 34 | owner: username.data.login, 35 | path: `post/${slug}`, 36 | repo: process.env.REPO_NAME || "", 37 | }); 38 | 39 | if (checkPostExist.data) { 40 | const objectPost = { 41 | title: title, 42 | path: `post/${slug}/content.html`, 43 | createAt: new Date(), 44 | slug: slug, 45 | }; 46 | 47 | const path = `post/${slug}/content.html`; 48 | const pr = await github.createPullRequest({ 49 | owner: username.data.login, 50 | repo: process.env.REPO_NAME || "", 51 | title: `Create new post ${title}`, 52 | body: "", 53 | head: slug, 54 | createWhenEmpty: true, 55 | changes: [ 56 | { 57 | /* optional: if `files` is not passed, an empty commit is created instead */ 58 | files: { 59 | [path]: content, 60 | }, 61 | commit: "NEW POST", 62 | }, 63 | ], 64 | }); 65 | 66 | if (pr) { 67 | await github.rest.pulls.merge({ 68 | owner: username.data.login, 69 | repo: process.env.REPO_NAME || "", 70 | pull_number: pr?.data.number, 71 | }); 72 | return res 73 | .status(200) 74 | .send({ 75 | data: { ...objectPost, path: path, content: content }, 76 | status: "success", 77 | }); 78 | } 79 | } 80 | } catch (error: any) { 81 | res.status(404).send({ data:null,status: "failed", errorMsg: error }); 82 | } 83 | } catch (error) { 84 | res.status(500).send({ data:null,status: "failed", errorMsg: error }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import GithubAPI from "utils/GithubAPI"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | import NextAuth from "next-auth"; 5 | import GithubProvider from "next-auth/providers/github"; 6 | export default async function auth(req: NextApiRequest, res: NextApiResponse) { 7 | const providers = [ 8 | GithubProvider({ 9 | clientId: process.env.OAUTH_CLIENT || "", 10 | clientSecret: process.env.OAUTH_SECRET || "", 11 | }), 12 | ]; 13 | 14 | return await NextAuth(req, res, { 15 | providers, 16 | callbacks: { 17 | async signIn({ user, account, profile, email, credentials }) { 18 | const github = GithubAPI(); 19 | const username = await github.rest.users.getAuthenticated(); 20 | if (username.data.id.toString() === user.id) { 21 | return true; 22 | } 23 | 24 | return "/admin/login?error=true"; 25 | }, 26 | }, 27 | secret: process.env.NEXTAUTH_SECRET, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Flex, 5 | Heading, 6 | Input, 7 | Link, 8 | Spinner, 9 | Text, 10 | } from "@chakra-ui/react"; 11 | import Card from "components/Card/Card"; 12 | import type { NextPage } from "next"; 13 | import useSWR from "swr"; 14 | import fetcher from "utils/fetcher"; 15 | import NextLink from "next/link"; 16 | import LayoutPublic from "components/Layout/LayoutPublic"; 17 | import { useState } from "react"; 18 | import usePaging from "components/Pagination/Paging"; 19 | import NextHead from "next/head"; 20 | 21 | const Home: NextPage = () => { 22 | const { data } = useSWR("/api/admin/post/get", fetcher); 23 | const [Paging, dataPaging] = usePaging(); 24 | const [search, setsearch] = useState(""); 25 | 26 | return ( 27 | setsearch(e.target.value), 32 | }} 33 | > 34 | 35 | Git Blog 36 | 37 | 38 | 49 | Git Blog 50 | Blog but with github 51 | 52 | 60 | {!data && ( 61 | 62 | 63 | 64 | )} 65 | 66 | {data && data.data.length == 0 && ( 67 | 68 | No Post Yet 😟 69 | 70 | )} 71 | 72 | {data && 73 | data.data.length > 0 && 74 | data.data 75 | .filter((el: any) => 76 | el.title.toLowerCase().includes(search.toLowerCase()) 77 | ) 78 | .slice(dataPaging.active, dataPaging.active + dataPaging.length) 79 | .map((el: any, i: number) => ( 80 | 81 | 82 | 83 | 84 | 85 | ))} 86 | 87 | {data && ( 88 | 90 | el.title.toLowerCase().includes(search.toLowerCase()) 91 | )} 92 | /> 93 | )} 94 | 95 | 96 | ); 97 | }; 98 | 99 | export default Home; 100 | -------------------------------------------------------------------------------- /pages/post/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Flex, Heading, Input, Spinner, Text } from "@chakra-ui/react"; 3 | import useTipTap from "components/Editor/useTipTap"; 4 | import LayoutPublic from "components/Layout/LayoutPublic"; 5 | import { useRouter } from "next/router"; 6 | import useSWR from "swr"; 7 | import { POST_TYPE, RESPONSE_POST } from "types/types"; 8 | import fetcher from "utils/fetcher"; 9 | import NextHead from "next/head"; 10 | 11 | export default function Detail() { 12 | const router = useRouter(); 13 | const { slug } = router.query; 14 | const { data: dataPost, error: errorPost } = useSWR>( 15 | slug ? `/api/admin/post/${slug}` : null, 16 | fetcher 17 | ); 18 | const [Editor] = useTipTap(dataPost?.data.content, false); 19 | 20 | function parseDate(date: Date) { 21 | return Intl.DateTimeFormat("en-US", { 22 | year: "numeric", 23 | month: "long", 24 | weekday: "long", 25 | day: "2-digit", 26 | }).format(new Date(date)); 27 | } 28 | 29 | return ( 30 | 31 | {!dataPost && ( 32 | 33 | 34 | 35 | )} 36 | {dataPost && ( 37 | <> 38 | 39 | {dataPost.data.title} 40 | 41 | 42 | 43 | {dataPost.data.title} 44 | 45 | 46 | {parseDate(dataPost.data.createAt)} 47 | 48 | 49 | 50 | 51 | 52 | 53 | )} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aqshola/git-blog/60d0a5d38981c991871fb367b44c5a63437ceb9d/public/favicon.ico -------------------------------------------------------------------------------- /public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aqshola/git-blog/60d0a5d38981c991871fb367b44c5a63437ceb9d/public/github.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | .ProseMirror-focused{ 2 | border: none !important; 3 | outline: none ; 4 | } 5 | 6 | .ProseMirror{ 7 | min-height: 200px; 8 | } 9 | 10 | 11 | .ProseMirror ul{ 12 | padding: 0 1rem; 13 | } 14 | 15 | .ProseMirror ol{ 16 | padding: 0 1rem; 17 | } 18 | 19 | .ProseMirror h1, 20 | .ProseMirror h2, 21 | .ProseMirror h3, 22 | .ProseMirror h4, 23 | .ProseMirror h5, 24 | .ProseMirror h6 { 25 | line-height: 1.1; 26 | } 27 | 28 | .ProseMirror h1 { 29 | display: block; 30 | font-size: 2em; 31 | margin-top: 0.67em; 32 | margin-bottom: 0.67em; 33 | margin-left: 0; 34 | margin-right: 0; 35 | font-weight: bold; 36 | } 37 | .ProseMirror h2 { 38 | display: block; 39 | font-size: 1.5em; 40 | margin-top: 0.83em; 41 | margin-bottom: 0.83em; 42 | margin-left: 0; 43 | margin-right: 0; 44 | font-weight: bold; 45 | } 46 | .ProseMirror h3 { 47 | display: block; 48 | font-size: 1.17em; 49 | margin-top: 1em; 50 | margin-bottom: 1em; 51 | margin-left: 0; 52 | margin-right: 0; 53 | font-weight: bold; 54 | } 55 | .ProseMirror h4 { 56 | display: block; 57 | margin-top: 1.33em; 58 | margin-bottom: 1.33em; 59 | margin-left: 0; 60 | margin-right: 0; 61 | font-weight: bold; 62 | } 63 | .ProseMirror h5 { 64 | display: block; 65 | font-size: .83em; 66 | margin-top: 1.67em; 67 | margin-bottom: 1.67em; 68 | margin-left: 0; 69 | margin-right: 0; 70 | font-weight: bold; 71 | } 72 | .ProseMirror h6 { 73 | display: block; 74 | font-size: .67em; 75 | margin-top: 2.33em; 76 | margin-bottom: 2.33em; 77 | margin-left: 0; 78 | margin-right: 0; 79 | font-weight: bold; 80 | } 81 | 82 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "." 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "components/Editor"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /types/types.ts: -------------------------------------------------------------------------------- 1 | export type POST_TYPE = { 2 | title: string; 3 | path: string; 4 | createAt: Date; 5 | slug: string; 6 | content?: string|any; 7 | }; 8 | 9 | export type RESPONSE_POST = { 10 | data: DATA_TYPE 11 | status: "success" | "failed"; 12 | errorMsg?: string|null|any; 13 | }; 14 | -------------------------------------------------------------------------------- /utils/GithubAPI.ts: -------------------------------------------------------------------------------- 1 | import { createPullRequest } from "octokit-plugin-create-pull-request"; 2 | import { Octokit, App } from "octokit"; 3 | 4 | export default function GithubAPI() { 5 | const PluggedInOctokit = Octokit.plugin(createPullRequest); 6 | const github = new PluggedInOctokit({ 7 | auth: process.env.GITHUB_KEY, 8 | }); 9 | 10 | return github; 11 | } 12 | -------------------------------------------------------------------------------- /utils/fetcher.ts: -------------------------------------------------------------------------------- 1 | export default async function fetcher(url: string) { 2 | const result = await fetch(url); 3 | const parsedResult = await result.json(); 4 | return await parsedResult; 5 | } 6 | -------------------------------------------------------------------------------- /utils/parseContentFromGithub.ts: -------------------------------------------------------------------------------- 1 | export default function parseContentFromGithub(githubData: any): T | undefined { 2 | if (!Array.isArray(githubData)) { 3 | let parsedGivenData = githubData as any; 4 | //@ts-nocheck 5 | let parsedData = Buffer.from( 6 | parsedGivenData.content, 7 | "base64" 8 | ).toString() as unknown as T; 9 | 10 | return parsedData; 11 | } 12 | } --------------------------------------------------------------------------------