├── .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 | 
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 | 
33 | 
34 | 
35 | 
36 | 
37 | 
38 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------