├── .eslintrc copy.json ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── @types └── @tiptap │ └── index.d.ts ├── README.md ├── components ├── content │ ├── CodeBlockComponent.jsx │ ├── ColourHighlighter.ts │ ├── SmilieReplacer.ts │ ├── StyledToggleButtonGroup.tsx │ ├── Toolbar.tsx │ ├── findColours.ts │ ├── index.tsx │ ├── style.tsx │ └── toolbars │ │ └── HeadingToolbarButtons.tsx ├── extension │ ├── Image │ │ ├── Component.tsx │ │ └── index.ts │ └── VideoPlayer │ │ ├── Component.tsx │ │ └── index.ts └── renderers │ ├── ImageRenderer.tsx │ └── VideoRenderer.tsx ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ └── hello.ts └── index.tsx ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css ├── theme ├── breakpoints.js ├── globalStyles.js ├── index.js ├── overrides │ ├── Autocomplete.js │ ├── Backdrop.js │ ├── Button.js │ ├── Card.js │ ├── IconButton.js │ ├── Input.js │ ├── Lists.js │ ├── Paper.js │ ├── Tooltip.js │ ├── Typography.js │ └── index.js ├── palette.js ├── shadows.js ├── shape.js └── typography.js └── tsconfig.json /.eslintrc copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["plugin:react/recommended", "next", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["@typescript-eslint", "@next/next"], 16 | "rules": { 17 | "react/jsx-filename-extension": [ 18 | 1, 19 | { "extensions": [".js", ".jsx", ".tsx"] } 20 | ], 21 | "react/jsx-props-no-spreading": [ 22 | 1, 23 | { 24 | "html": "ignore", 25 | "explicitSpread": "ignore" 26 | } 27 | ], 28 | "import/extensions": [1, "never"], 29 | "no-use-before-define": "off", 30 | "react/require-default-props": [ 31 | 1, 32 | { "forbidDefaultForRequired": true, "ignoreFunctionalComponents": true } 33 | ], 34 | "react/forbid-prop-types": [1] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /@types/@tiptap/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from "@tiptap/core"; 2 | 3 | import { optionsI } from "components/extension/VideoPlayer/index"; 4 | 5 | declare module "@tiptap/core" { 6 | interface Commands { 7 | VideoPlayerExtension: { 8 | /** 9 | * Comments will be added to the autocomplete. 10 | */ 11 | setVideo: (someProp: optionsI) => ReturnType; 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /components/content/CodeBlockComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NodeViewWrapper, NodeViewContent } from "@tiptap/react"; 3 | import styled from "@emotion/styled"; 4 | 5 | const NodeViewWrapperStyled = styled(NodeViewWrapper)` 6 | position: relative; 7 | 8 | select { 9 | position: absolute; 10 | right: 0.5rem; 11 | top: 0.5rem; 12 | } 13 | `; 14 | 15 | export default function CodeBlockComponent(props) { 16 | return ( 17 | 18 | 33 |
34 |         
35 |       
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/content/ColourHighlighter.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from "@tiptap/core"; 2 | import { Plugin } from "prosemirror-state"; 3 | import findColors from "./findColours"; 4 | 5 | export const ColorHighlighter = Extension.create({ 6 | name: "colorHighlighter", 7 | 8 | addProseMirrorPlugins() { 9 | return [ 10 | new Plugin({ 11 | state: { 12 | init(_, { doc }) { 13 | return findColors(doc); 14 | }, 15 | apply(transaction, oldState) { 16 | return transaction.docChanged 17 | ? findColors(transaction.doc) 18 | : oldState; 19 | }, 20 | }, 21 | props: { 22 | decorations(state) { 23 | return this.getState(state); 24 | }, 25 | }, 26 | }), 27 | ]; 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /components/content/SmilieReplacer.ts: -------------------------------------------------------------------------------- 1 | import { Extension, textInputRule } from "@tiptap/core"; 2 | 3 | export const SmilieReplacer = Extension.create({ 4 | name: "smilieReplacer", 5 | 6 | addInputRules() { 7 | return [ 8 | textInputRule({ find: /-___- $/, replace: "😑 " }), 9 | textInputRule({ find: /:'-\) $/, replace: "😂 " }), 10 | textInputRule({ find: /':-\) $/, replace: "😅 " }), 11 | textInputRule({ find: /':-D $/, replace: "😅 " }), 12 | textInputRule({ find: />:-\) $/, replace: "😆 " }), 13 | textInputRule({ find: /-__- $/, replace: "😑 " }), 14 | textInputRule({ find: /':-\( $/, replace: "😓 " }), 15 | textInputRule({ find: /:'-\( $/, replace: "😢 " }), 16 | textInputRule({ find: />:-\( $/, replace: "😠 " }), 17 | textInputRule({ find: /O:-\) $/, replace: "😇 " }), 18 | textInputRule({ find: /0:-3 $/, replace: "😇 " }), 19 | textInputRule({ find: /0:-\) $/, replace: "😇 " }), 20 | textInputRule({ find: /0;\^\) $/, replace: "😇 " }), 21 | textInputRule({ find: /O;-\) $/, replace: "😇 " }), 22 | textInputRule({ find: /0;-\) $/, replace: "😇 " }), 23 | textInputRule({ find: /O:-3 $/, replace: "😇 " }), 24 | textInputRule({ find: /:'\) $/, replace: "😂 " }), 25 | textInputRule({ find: /:-D $/, replace: "😃 " }), 26 | textInputRule({ find: /':\) $/, replace: "😅 " }), 27 | textInputRule({ find: /'=\) $/, replace: "😅 " }), 28 | textInputRule({ find: /':D $/, replace: "😅 " }), 29 | textInputRule({ find: /'=D $/, replace: "😅 " }), 30 | textInputRule({ find: />:\) $/, replace: "😆 " }), 31 | textInputRule({ find: />;\) $/, replace: "😆 " }), 32 | textInputRule({ find: />=\) $/, replace: "😆 " }), 33 | textInputRule({ find: /;-\) $/, replace: "😉 " }), 34 | textInputRule({ find: /\*-\) $/, replace: "😉 " }), 35 | textInputRule({ find: /;-\] $/, replace: "😉 " }), 36 | textInputRule({ find: /;\^\) $/, replace: "😉 " }), 37 | textInputRule({ find: /B-\) $/, replace: "😎 " }), 38 | textInputRule({ find: /8-\) $/, replace: "😎 " }), 39 | textInputRule({ find: /B-D $/, replace: "😎 " }), 40 | textInputRule({ find: /8-D $/, replace: "😎 " }), 41 | textInputRule({ find: /:-\* $/, replace: "😘 " }), 42 | textInputRule({ find: /:\^\* $/, replace: "😘 " }), 43 | textInputRule({ find: /:-\) $/, replace: "🙂 " }), 44 | textInputRule({ find: /-_- $/, replace: "😑 " }), 45 | textInputRule({ find: /:-X $/, replace: "😶 " }), 46 | textInputRule({ find: /:-# $/, replace: "😶 " }), 47 | textInputRule({ find: /:-x $/, replace: "😶 " }), 48 | textInputRule({ find: />.< $/, replace: "😣 " }), 49 | textInputRule({ find: /:-O $/, replace: "😮 " }), 50 | textInputRule({ find: /:-o $/, replace: "😮 " }), 51 | textInputRule({ find: /O_O $/, replace: "😮 " }), 52 | textInputRule({ find: />:O $/, replace: "😮 " }), 53 | textInputRule({ find: /:-P $/, replace: "😛 " }), 54 | textInputRule({ find: /:-p $/, replace: "😛 " }), 55 | textInputRule({ find: /:-Þ $/, replace: "😛 " }), 56 | textInputRule({ find: /:-þ $/, replace: "😛 " }), 57 | textInputRule({ find: /:-b $/, replace: "😛 " }), 58 | textInputRule({ find: />:P $/, replace: "😜 " }), 59 | textInputRule({ find: /X-P $/, replace: "😜 " }), 60 | textInputRule({ find: /x-p $/, replace: "😜 " }), 61 | textInputRule({ find: /':\( $/, replace: "😓 " }), 62 | textInputRule({ find: /'=\( $/, replace: "😓 " }), 63 | textInputRule({ find: />:\\ $/, replace: "😕 " }), 64 | textInputRule({ find: />:\/ $/, replace: "😕 " }), 65 | textInputRule({ find: /:-\/ $/, replace: "😕 " }), 66 | textInputRule({ find: /:-. $/, replace: "😕 " }), 67 | textInputRule({ find: />:\[ $/, replace: "😞 " }), 68 | textInputRule({ find: /:-\( $/, replace: "😞 " }), 69 | textInputRule({ find: /:-\[ $/, replace: "😞 " }), 70 | textInputRule({ find: /:'\( $/, replace: "😢 " }), 71 | textInputRule({ find: /;-\( $/, replace: "😢 " }), 72 | textInputRule({ find: /#-\) $/, replace: "😵 " }), 73 | textInputRule({ find: /%-\) $/, replace: "😵 " }), 74 | textInputRule({ find: /X-\) $/, replace: "😵 " }), 75 | textInputRule({ find: />:\( $/, replace: "😠 " }), 76 | textInputRule({ find: /0:3 $/, replace: "😇 " }), 77 | textInputRule({ find: /0:\) $/, replace: "😇 " }), 78 | textInputRule({ find: /O:\) $/, replace: "😇 " }), 79 | textInputRule({ find: /O=\) $/, replace: "😇 " }), 80 | textInputRule({ find: /O:3 $/, replace: "😇 " }), 81 | textInputRule({ find: /<\/3 $/, replace: "💔 " }), 82 | textInputRule({ find: /:D $/, replace: "😃 " }), 83 | textInputRule({ find: /=D $/, replace: "😃 " }), 84 | textInputRule({ find: /;\) $/, replace: "😉 " }), 85 | textInputRule({ find: /\*\) $/, replace: "😉 " }), 86 | textInputRule({ find: /;\] $/, replace: "😉 " }), 87 | textInputRule({ find: /;D $/, replace: "😉 " }), 88 | textInputRule({ find: /B\) $/, replace: "😎 " }), 89 | textInputRule({ find: /8\) $/, replace: "😎 " }), 90 | textInputRule({ find: /:\* $/, replace: "😘 " }), 91 | textInputRule({ find: /=\* $/, replace: "😘 " }), 92 | textInputRule({ find: /:\) $/, replace: "🙂 " }), 93 | textInputRule({ find: /=\] $/, replace: "🙂 " }), 94 | textInputRule({ find: /=\) $/, replace: "🙂 " }), 95 | textInputRule({ find: /:\] $/, replace: "🙂 " }), 96 | textInputRule({ find: /:X $/, replace: "😶 " }), 97 | textInputRule({ find: /:# $/, replace: "😶 " }), 98 | textInputRule({ find: /=X $/, replace: "😶 " }), 99 | textInputRule({ find: /=x $/, replace: "😶 " }), 100 | textInputRule({ find: /:x $/, replace: "😶 " }), 101 | textInputRule({ find: /=# $/, replace: "😶 " }), 102 | textInputRule({ find: /:O $/, replace: "😮 " }), 103 | textInputRule({ find: /:o $/, replace: "😮 " }), 104 | textInputRule({ find: /:P $/, replace: "😛 " }), 105 | textInputRule({ find: /=P $/, replace: "😛 " }), 106 | textInputRule({ find: /:p $/, replace: "😛 " }), 107 | textInputRule({ find: /=p $/, replace: "😛 " }), 108 | textInputRule({ find: /:Þ $/, replace: "😛 " }), 109 | textInputRule({ find: /:þ $/, replace: "😛 " }), 110 | textInputRule({ find: /:b $/, replace: "😛 " }), 111 | textInputRule({ find: /d: $/, replace: "😛 " }), 112 | textInputRule({ find: /:\/ $/, replace: "😕 " }), 113 | textInputRule({ find: /:\\ $/, replace: "😕 " }), 114 | textInputRule({ find: /=\/ $/, replace: "😕 " }), 115 | textInputRule({ find: /=\\ $/, replace: "😕 " }), 116 | textInputRule({ find: /:L $/, replace: "😕 " }), 117 | textInputRule({ find: /=L $/, replace: "😕 " }), 118 | textInputRule({ find: /:\( $/, replace: "😞 " }), 119 | textInputRule({ find: /:\[ $/, replace: "😞 " }), 120 | textInputRule({ find: /=\( $/, replace: "😞 " }), 121 | textInputRule({ find: /;\( $/, replace: "😢 " }), 122 | textInputRule({ find: /D: $/, replace: "😨 " }), 123 | textInputRule({ find: /:\$ $/, replace: "😳 " }), 124 | textInputRule({ find: /=\$ $/, replace: "😳 " }), 125 | textInputRule({ find: /#\) $/, replace: "😵 " }), 126 | textInputRule({ find: /%\) $/, replace: "😵 " }), 127 | textInputRule({ find: /X\) $/, replace: "😵 " }), 128 | textInputRule({ find: /:@ $/, replace: "😠 " }), 129 | textInputRule({ find: /<3 $/, replace: "❤️ " }), 130 | textInputRule({ find: /\/shrug $/, replace: "¯\\_(ツ)_/¯" }), 131 | ]; 132 | }, 133 | }); 134 | -------------------------------------------------------------------------------- /components/content/StyledToggleButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({ 5 | "& .MuiToggleButtonGroup-grouped": { 6 | margin: theme.spacing(0.5), 7 | border: 0, 8 | "&.Mui-disabled": { 9 | border: 0, 10 | }, 11 | "&:not(:first-of-type)": { 12 | borderRadius: theme.shape.borderRadius, 13 | }, 14 | "&:first-of-type": { 15 | borderRadius: theme.shape.borderRadius, 16 | }, 17 | }, 18 | })); 19 | 20 | export default StyledToggleButtonGroup; 21 | -------------------------------------------------------------------------------- /components/content/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Editor } from "@tiptap/react"; 4 | 5 | import LinkIcon from "@mui/icons-material/Link"; 6 | import ImageIcon from "@mui/icons-material/Image"; 7 | 8 | import UndoIcon from "@mui/icons-material/Undo"; 9 | import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft"; 10 | import FormatAlignCenterIcon from "@mui/icons-material/FormatAlignCenter"; 11 | import FormatAlignRightIcon from "@mui/icons-material/FormatAlignRight"; 12 | import FormatAlignJustifyIcon from "@mui/icons-material/FormatAlignJustify"; 13 | import RedoIcon from "@mui/icons-material/Redo"; 14 | import CodeIcon from "@mui/icons-material/Code"; 15 | import FormatBoldIcon from "@mui/icons-material/FormatBold"; 16 | import FormatItalicIcon from "@mui/icons-material/FormatItalic"; 17 | import ClearIcon from "@mui/icons-material/Clear"; 18 | import LayersClearIcon from "@mui/icons-material/LayersClear"; 19 | import FormatTextdirectionRToLIcon from "@mui/icons-material/FormatTextdirectionRToL"; 20 | import FormatStrikethroughIcon from "@mui/icons-material/FormatStrikethrough"; 21 | import SubscriptIcon from "@mui/icons-material/Subscript"; 22 | import SuperscriptIcon from "@mui/icons-material/Superscript"; 23 | import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined"; 24 | import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; 25 | import FormatQuoteIcon from "@mui/icons-material/FormatQuote"; 26 | import BorderColorIcon from "@mui/icons-material/BorderColor"; 27 | import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; 28 | import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered"; 29 | import VideoLibraryIcon from "@mui/icons-material/VideoLibrary"; 30 | 31 | import Paper from "@mui/material/Paper"; 32 | 33 | import ToggleButton from "@mui/material/ToggleButton"; 34 | import { Typography, Divider } from "@mui/material"; 35 | 36 | import PickImage from "../renderers/ImageRenderer"; 37 | import PickVideo from "../renderers/VideoRenderer"; 38 | 39 | import StyledToggleButtonGroup from "components/content/StyledToggleButtonGroup"; 40 | 41 | import HeadingToolbarButtons from "components/content/toolbars/HeadingToolbarButtons"; 42 | 43 | const ProjectCreateContentToolbar = ({ editor }: { editor: Editor }) => { 44 | const [OpenPickImage, setOpenPickImage] = React.useState(false); 45 | const [OpenPickVideo, setOpenPickVideo] = React.useState(false); 46 | 47 | if (!editor) { 48 | return null; 49 | } 50 | 51 | return ( 52 | <> 53 | `1px solid ${theme.palette.divider}`, 58 | flexWrap: "wrap", 59 | mb: 2, 60 | position: "sticky", 61 | top: 10, 62 | zIndex: 9999, 63 | }} 64 | > 65 | 66 | 67 | 72 | editor.chain().focus().setTextAlign("left").run()} 74 | selected={editor.isActive({ textAlign: "left" })} 75 | value="left" 76 | aria-label="left aligned" 77 | > 78 | 79 | 80 | editor.chain().focus().setTextAlign("center").run()} 82 | selected={editor.isActive({ textAlign: "center" })} 83 | value="center" 84 | aria-label="Center aligned" 85 | > 86 | 87 | 88 | editor.chain().focus().setTextAlign("right").run()} 90 | selected={editor.isActive({ textAlign: "right" })} 91 | value="right" 92 | aria-label="Right aligned" 93 | > 94 | 95 | 96 | editor.chain().focus().setTextAlign("justify").run()} 98 | selected={editor.isActive({ textAlign: "justify" })} 99 | value="justify" 100 | aria-label="Justify aligned" 101 | > 102 | 103 | 104 | 105 | 106 | 107 | 108 | editor.chain().focus().toggleSuperscript().run()} 110 | selected={editor.isActive("superscript")} 111 | value="superscript" 112 | aria-label="superscript" 113 | > 114 | 115 | 116 | editor.chain().focus().toggleSubscript().run()} 118 | selected={editor.isActive("subscript")} 119 | value="subscript" 120 | aria-label="subscript" 121 | > 122 | 123 | 124 | editor.chain().focus().toggleBold().run()} 126 | selected={editor.isActive("bold")} 127 | value="bold" 128 | aria-label="bold" 129 | > 130 | 131 | 132 | 133 | editor.chain().focus().toggleItalic().run()} 135 | value="italic" 136 | aria-label="italic" 137 | selected={editor.isActive("italic")} 138 | > 139 | 140 | 141 | editor.chain().focus().toggleStrike().run()} 143 | value="strike" 144 | aria-label="strike" 145 | selected={editor.isActive("strike")} 146 | > 147 | 148 | 149 | editor.chain().focus().toggleCode().run()} 151 | value="code" 152 | aria-label="code" 153 | selected={editor.isActive("code")} 154 | > 155 | 156 | 157 | 158 | editor.chain().focus().toggleHighlight().run()} 160 | value="highlight" 161 | aria-label="highlight" 162 | selected={editor.isActive("highlight")} 163 | > 164 | 165 | 166 | editor.chain().focus().toggleBlockquote().run()} 168 | value="blockQuote" 169 | aria-label="blockQuote" 170 | selected={editor.isActive("blockQuote")} 171 | > 172 | 173 | 174 | editor.chain().focus().setHorizontalRule().run()} 176 | selected={editor.isActive("HorizontalRule")} 177 | value="HorizontalRule" 178 | aria-label="HorizontalRule" 179 | > 180 | 181 | 182 | editor.chain().focus().setParagraph().run()} 184 | selected={editor.isActive("paragraph")} 185 | value="paragraph" 186 | aria-label="paragraph" 187 | > 188 | 189 | 190 | editor.chain().focus().toggleUnderline().run()} 192 | selected={editor.isActive("underline")} 193 | value="underline" 194 | aria-label="underline" 195 | > 196 | 197 | 198 | { 200 | setOpenPickImage(true); 201 | }} 202 | selected={editor.isActive("image-renderer")} 203 | value="image-renderer" 204 | aria-label="image-renderer" 205 | > 206 | 207 | 208 | setOpenPickImage(false)} 211 | setThumbnail={(value: { src: string; alt?: string }) => { 212 | editor 213 | .chain() 214 | .focus() 215 | // @ts-ignore 216 | .setImage({ src: value.src, alt: value.alt }) 217 | .run(); 218 | }} 219 | /> 220 | { 222 | console.log(editor.state); 223 | setOpenPickVideo(true); 224 | }} 225 | selected={editor.isActive("videoPlayer")} 226 | value="videoPlayer" 227 | aria-label="videoPlayer" 228 | > 229 | 230 | 231 | setOpenPickVideo(false)} 234 | setThumbnail={(value: { src: string }) => { 235 | editor.chain().focus().setVideo({ src: value.src }).run(); 236 | }} 237 | /> 238 | { 240 | const previousUrl = editor.getAttributes("link").href; 241 | const url = window.prompt("URL", previousUrl); 242 | 243 | // cancelled 244 | if (url === null) { 245 | return; 246 | } 247 | 248 | // empty 249 | if (url === "") { 250 | editor 251 | .chain() 252 | .focus() 253 | .extendMarkRange("link") 254 | .unsetLink() 255 | .run(); 256 | 257 | return; 258 | } 259 | 260 | // update link 261 | editor 262 | .chain() 263 | .focus() 264 | .extendMarkRange("link") 265 | .setLink({ href: url }) 266 | .run(); 267 | }} 268 | selected={editor.isActive("link")} 269 | value="link" 270 | aria-label="link" 271 | > 272 | 273 | 274 | editor.chain().focus().toggleBulletList().run()} 276 | value="bullettList" 277 | aria-label="bullettList" 278 | selected={editor.isActive("bulletList")} 279 | > 280 | 281 | 282 | editor.chain().focus().toggleOrderedList().run()} 284 | value="orderedList" 285 | aria-label="orderedList" 286 | selected={editor.isActive("orderedList")} 287 | > 288 | 289 | 290 | 291 | 292 | 293 | 298 | editor.chain().focus().undo().run()} 300 | value="undo" 301 | aria-label="undo" 302 | > 303 | 304 | 305 | editor.chain().focus().redo().run()} 307 | value="redo" 308 | aria-label="redo" 309 | > 310 | 311 | 312 | 313 | 314 | 315 | 316 | 321 | editor.chain().focus().unsetAllMarks().run()} 323 | value="clear-mark" 324 | aria-label="clear-mark" 325 | > 326 | 327 | 328 | editor.chain().focus().clearNodes().run()} 330 | value="clear-node" 331 | aria-label="clear-node" 332 | > 333 | 334 | 335 | 336 | 337 | 338 | ); 339 | }; 340 | 341 | export default ProjectCreateContentToolbar; 342 | -------------------------------------------------------------------------------- /components/content/findColours.ts: -------------------------------------------------------------------------------- 1 | import { Decoration, DecorationSet } from "prosemirror-view"; 2 | import { Node } from "prosemirror-model"; 3 | 4 | export default function findColours(doc: Node): DecorationSet { 5 | const hexColor = /(#[0-9a-f]{3,6})\b/gi; 6 | const decorations: Decoration[] = []; 7 | 8 | doc.descendants((node, position) => { 9 | if (!node.text) { 10 | return; 11 | } 12 | 13 | Array.from(node.text.matchAll(hexColor)).forEach((match) => { 14 | const color = match[0]; 15 | const index = match.index || 0; 16 | const from = position + index; 17 | const to = from + color.length; 18 | const decoration = Decoration.inline(from, to, { 19 | class: "color", 20 | style: `--color: ${color}`, 21 | }); 22 | 23 | decorations.push(decoration); 24 | }); 25 | }); 26 | 27 | return DecorationSet.create(doc, decorations); 28 | } 29 | -------------------------------------------------------------------------------- /components/content/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | useEditor, 5 | EditorContent, 6 | Editor, 7 | ReactNodeViewRenderer, 8 | } from "@tiptap/react"; 9 | import StarterKit from "@tiptap/starter-kit"; 10 | import Highlight from "@tiptap/extension-highlight"; 11 | import TypographyExtension from "@tiptap/extension-typography"; 12 | import UnderlineExtension from "@tiptap/extension-underline"; 13 | import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; 14 | import Document from "@tiptap/extension-document"; 15 | import Paragraph from "@tiptap/extension-paragraph"; 16 | import Text from "@tiptap/extension-text"; 17 | import Dropcursor from "@tiptap/extension-dropcursor"; 18 | import CharacterCount from "@tiptap/extension-character-count"; 19 | import Link from "@tiptap/extension-link"; 20 | import Code from "@tiptap/extension-code"; 21 | import TextAlign from "@tiptap/extension-text-align"; 22 | import Focus from "@tiptap/extension-focus"; 23 | import Superscript from "@tiptap/extension-superscript"; 24 | import Subscript from "@tiptap/extension-subscript"; 25 | import VideoPlayerExtension from "../extension/VideoPlayer"; 26 | import Image from "../extension/Image"; 27 | // import Image from "@tiptap/extension-image"; 28 | // Image. 29 | // VideoPlayerExtension. 30 | import { ColorHighlighter } from "./ColourHighlighter"; 31 | import { SmilieReplacer } from "./SmilieReplacer"; 32 | 33 | import { styled } from "@mui/material/styles"; 34 | 35 | import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; 36 | 37 | import ProjectCreateContentToolbar from "./Toolbar"; 38 | 39 | const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({ 40 | "& .MuiToggleButtonGroup-grouped": { 41 | margin: theme.spacing(0.5), 42 | border: 0, 43 | "&.Mui-disabled": { 44 | border: 0, 45 | }, 46 | "&:not(:first-of-type)": { 47 | borderRadius: theme.shape.borderRadius, 48 | }, 49 | "&:first-of-type": { 50 | borderRadius: theme.shape.borderRadius, 51 | }, 52 | }, 53 | })); 54 | 55 | // import "./styles.scss"; 56 | import EditorStyled from "./style"; 57 | 58 | export default function EditorComponent({ 59 | // setContent, 60 | content, 61 | }: { 62 | // setContent: (value: string) => void; 63 | content: string; 64 | }) { 65 | const limit = 5000; 66 | const editor = useEditor({ 67 | extensions: [ 68 | StarterKit, 69 | Subscript, 70 | Superscript, 71 | Highlight, 72 | TypographyExtension, 73 | UnderlineExtension, 74 | Document, 75 | Paragraph, 76 | Text, 77 | 78 | Dropcursor, 79 | Code, 80 | Link, 81 | CodeBlockLowlight, 82 | CharacterCount.configure({ 83 | limit, 84 | }), 85 | TextAlign.configure({ 86 | types: ["heading", "paragraph"], 87 | }), 88 | Focus.configure({ 89 | className: "has-focus", 90 | mode: "all", 91 | }), 92 | ColorHighlighter, 93 | SmilieReplacer, 94 | VideoPlayerExtension, 95 | Image, 96 | ], 97 | content: content, 98 | }); 99 | 100 | React.useMemo(() => { 101 | // if (editor?.getHTML()) setContent(editor?.getHTML()); 102 | }, [editor?.getHTML()]); 103 | 104 | const percentage = editor 105 | ? Math.round((100 / limit) * editor.getCharacterCount()) 106 | : 0; 107 | return ( 108 | 109 | {editor && } 110 | 111 | {editor && ( 112 |
125 | 131 | 132 | 142 | 143 | 144 | 145 |
146 | {editor.getCharacterCount()}/{limit} characters 147 |
148 |
149 | )} 150 |
151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /components/content/style.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | const EditorStyled = styled.div` 4 | /* Basic editor styles */ 5 | 6 | div[contenteditable="true"] { 7 | border: none; 8 | background-color: transparent; 9 | outline: none; 10 | } 11 | 12 | code { 13 | background-color: #f4f6f8; 14 | border-radius: 5px; 15 | padding: 5px 5px; 16 | } 17 | 18 | .has-focus { 19 | border-radius: 3px; 20 | box-shadow: 0 0 0 3px #00ab55; 21 | padding: 5px 5px; 22 | } 23 | 24 | /* Color swatches */ 25 | .color { 26 | white-space: nowrap; 27 | 28 | &::before { 29 | content: " "; 30 | display: inline-block; 31 | width: 1em; 32 | height: 1em; 33 | border: 1px solid rgba(128, 128, 128, 0.3); 34 | vertical-align: middle; 35 | margin-right: 0.1em; 36 | margin-bottom: 0.15em; 37 | border-radius: 2px; 38 | background-color: var(--color); 39 | } 40 | } 41 | 42 | /* Placeholder (at the top) */ 43 | .ProseMirror p.is-editor-empty:first-child::before { 44 | content: attr(data-placeholder); 45 | float: left; 46 | color: #adb5bd; 47 | pointer-events: none; 48 | height: 0; 49 | } 50 | 51 | .draggable-item { 52 | display: flex; 53 | padding: 0.5rem; 54 | margin: 0.5rem 0; 55 | border-radius: 0.5rem; 56 | cursor: grab; 57 | transition: 300ms; 58 | 59 | :hover { 60 | background: white; 61 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 62 | 0px 10px 20px rgba(0, 0, 0, 0.1); 63 | } 64 | 65 | .drag-handle { 66 | flex: 0 0 auto; 67 | position: relative; 68 | 69 | top: 0.3rem; 70 | margin-right: 0.5rem; 71 | cursor: grab; 72 | background-image: url('data:image/svg+xml;charset=UTF-8,'); 73 | background-repeat: no-repeat; 74 | background-size: contain; 75 | background-position: center; 76 | } 77 | 78 | .content { 79 | flex: 1 1 auto; 80 | } 81 | } 82 | 83 | .ProseMirror { 84 | > * + * { 85 | margin-top: 1em; 86 | } 87 | 88 | img { 89 | max-width: 100%; 90 | height: auto; 91 | margin: auto; 92 | 93 | &.ProseMirror-selectednode { 94 | outline: 3px solid #00ab55; 95 | } 96 | } 97 | 98 | blockquote { 99 | padding-left: 1rem; 100 | border-left: 2px solid #00ab55; 101 | } 102 | ul, 103 | ol { 104 | padding: 0 1.5rem; 105 | } 106 | 107 | h1, 108 | h2, 109 | h3, 110 | h4, 111 | h5, 112 | h6 { 113 | line-height: 1.1; 114 | } 115 | 116 | code { 117 | background-color: rgba(#616161, 0.1); 118 | color: #616161; 119 | } 120 | 121 | pre { 122 | background: #0d0d0d; 123 | color: #fff; 124 | font-family: "JetBrainsMono", monospace; 125 | padding: 0.75rem 1rem; 126 | border-radius: 0.5rem; 127 | 128 | code { 129 | color: inherit; 130 | padding: 0; 131 | background: none; 132 | font-size: 0.8rem; 133 | } 134 | } 135 | 136 | img { 137 | max-width: 100%; 138 | height: auto; 139 | } 140 | 141 | hr { 142 | border-top: 1px solid #0d0d0d; 143 | margin: auto; 144 | width: 50%; 145 | margin-top: 10px; 146 | margin-bottom: 10px; 147 | } 148 | } 149 | `; 150 | 151 | export default EditorStyled; 152 | -------------------------------------------------------------------------------- /components/content/toolbars/HeadingToolbarButtons.tsx: -------------------------------------------------------------------------------- 1 | import { Editor } from "@tiptap/react"; 2 | 3 | import StyledToggleButtonGroup from "components/content/StyledToggleButtonGroup"; 4 | import ToggleButton from "@mui/material/ToggleButton"; 5 | import { Typography } from "@mui/material"; 6 | 7 | export default function HeadingToolbarButtons({ editor }: { editor: Editor }) { 8 | return ( 9 | 10 | editor.chain().focus().toggleHeading({ level: 1 }).run()} 14 | selected={editor.isActive("heading", { level: 1 })} 15 | > 16 | H1 17 | 18 | editor.chain().focus().toggleHeading({ level: 2 }).run()} 20 | selected={editor.isActive("heading", { level: 2 })} 21 | value="h2" 22 | aria-label="H2 Text" 23 | > 24 | H2 25 | 26 | editor.chain().focus().toggleHeading({ level: 3 }).run()} 28 | selected={editor.isActive("heading", { level: 3 })} 29 | value="h3" 30 | aria-label="H3 Text" 31 | > 32 | H3 33 | 34 | editor.chain().focus().toggleHeading({ level: 4 }).run()} 36 | selected={editor.isActive("heading", { level: 4 })} 37 | value="h4" 38 | aria-label="H4 Text" 39 | > 40 | H4 41 | 42 | editor.chain().focus().toggleHeading({ level: 5 }).run()} 44 | selected={editor.isActive("heading", { level: 5 })} 45 | value="h5" 46 | aria-label="H5 Text" 47 | > 48 | H5 49 | 50 | editor.chain().focus().toggleHeading({ level: 6 }).run()} 52 | selected={editor.isActive("heading", { level: 6 })} 53 | value="h6" 54 | aria-label="H6 Text" 55 | > 56 | H6 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/extension/Image/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NodeViewWrapper, NodeViewRendererProps } from "@tiptap/react"; 3 | import { LazyLoadImage } from "react-lazy-load-image-component"; 4 | import Container from "@mui/material/Container"; 5 | import Typography from "@mui/material/Typography"; 6 | 7 | export default function Image(props: NodeViewRendererProps) { 8 | return ( 9 | 16 |
17 | 21 | 27 | 28 | {props.node.attrs.alt} 29 | 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/extension/Image/index.ts: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes, CommandProps } from "@tiptap/core"; 2 | import { ReactNodeViewRenderer } from "@tiptap/react"; 3 | import Component from "./Component"; 4 | 5 | interface optionsI { 6 | src: string; 7 | alt?: string; 8 | } 9 | 10 | interface ImageCommands { 11 | imageRenderer: { 12 | /** 13 | * Add an image 14 | */ 15 | setImage: (options: optionsI) => ReturnType; 16 | }; 17 | } 18 | 19 | export interface ImageOptions { 20 | inline: boolean; 21 | HTMLAttributes: Record; 22 | } 23 | 24 | const ImageNode = Node.create>({ 25 | name: "image-renderer", 26 | 27 | group: "block", 28 | 29 | content: "block+", 30 | inline: false, 31 | 32 | draggable: true, 33 | addAttributes() { 34 | return { 35 | src: { 36 | default: null, 37 | }, 38 | alt: { 39 | default: null, 40 | }, 41 | }; 42 | }, 43 | 44 | parseHTML() { 45 | return [ 46 | { 47 | tag: "image-renderer[src]", 48 | }, 49 | ]; 50 | }, 51 | 52 | // @ts-ignore 53 | addCommands() { 54 | return { 55 | setImage: 56 | (options: optionsI) => 57 | ({ commands }: CommandProps) => { 58 | return commands.insertContent({ 59 | type: this.name, 60 | attrs: options, 61 | }); 62 | }, 63 | }; 64 | }, 65 | 66 | renderHTML({ HTMLAttributes }) { 67 | return ["image-renderer", mergeAttributes(HTMLAttributes)]; 68 | }, 69 | 70 | addNodeView() { 71 | return ReactNodeViewRenderer(Component); 72 | }, 73 | }); 74 | 75 | export default ImageNode; 76 | -------------------------------------------------------------------------------- /components/extension/VideoPlayer/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NodeViewWrapper, NodeViewRendererProps } from "@tiptap/react"; 3 | import { Player, Video, DefaultUi } from "@vime/react"; 4 | import Container from "@mui/material/Container"; 5 | 6 | function getId(url: string) { 7 | const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; 8 | const match = url.match(regExp); 9 | 10 | return match && match[2].length === 11 ? match[2] : null; 11 | } 12 | export default function VideoPlayer(props: NodeViewRendererProps) { 13 | const subtitles: Array<{ label: string; srcLang: string; src: string }> = 14 | props.node.attrs.subtitles; 15 | return ( 16 | 23 | {getId(props.node.attrs.src) ? ( 24 | 28 | {" "} 29 | 38 | 39 | ) : ( 40 | 41 | 42 | 55 | 56 | {/* We've replaced the `` component. */} 57 | {/* We can turn off any features we don't want via properties. */} 58 | 59 | {/* We can place our own UI components here to extend the default UI. */} 60 | 61 | 62 | 63 | )} 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/extension/VideoPlayer/index.ts: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes, CommandProps } from "@tiptap/core"; 2 | import { ReactNodeViewRenderer } from "@tiptap/react"; 3 | import Component from "./Component"; 4 | 5 | export interface optionsI { 6 | src: string; 7 | subtitles?: Array<{ label: string; srcLang: string; src: string }>; 8 | poster?: string; 9 | } 10 | 11 | interface VideoPlayerCommands { 12 | videoPlayer: { 13 | /** 14 | * Add an image 15 | */ 16 | setVideo: (options: optionsI) => ReturnType; 17 | }; 18 | } 19 | 20 | export interface ImageOptions { 21 | inline: boolean; 22 | HTMLAttributes: Record; 23 | } 24 | 25 | const VideoPlayerNode = Node.create>({ 26 | name: "videoPlayer", 27 | 28 | group: "block", 29 | 30 | content: "block+", 31 | inline: false, 32 | 33 | draggable: true, 34 | 35 | addAttributes() { 36 | return { 37 | src: { 38 | default: null, 39 | }, 40 | subtitles: { 41 | default: null, 42 | parseHTML: (element) => 43 | JSON.parse(String(element.getAttribute("data-subtitles"))), 44 | }, 45 | poster: { 46 | default: null, 47 | }, 48 | }; 49 | }, 50 | 51 | parseHTML() { 52 | return [ 53 | { 54 | tag: 'video-player[data-type="draggable-item"]', 55 | }, 56 | ]; 57 | }, 58 | 59 | // @ts-ignore 60 | addCommands() { 61 | return { 62 | setVideo: 63 | (options: optionsI) => 64 | ({ commands }: CommandProps) => { 65 | return commands.insertContent({ 66 | type: this.name, 67 | attrs: options, 68 | }); 69 | }, 70 | }; 71 | }, 72 | 73 | renderHTML({ HTMLAttributes }) { 74 | return [ 75 | "video-player", 76 | mergeAttributes(HTMLAttributes, { "data-type": "draggable-item" }), 77 | 0, 78 | ]; 79 | }, 80 | 81 | addNodeView() { 82 | return ReactNodeViewRenderer(Component); 83 | }, 84 | }); 85 | 86 | export default VideoPlayerNode; 87 | -------------------------------------------------------------------------------- /components/renderers/ImageRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Dialog from "@mui/material/Dialog"; 3 | import DialogActions from "@mui/material/DialogActions"; 4 | import DialogContent from "@mui/material/DialogContent"; 5 | import DialogTitle from "@mui/material/DialogTitle"; 6 | import { LazyLoadImage } from "react-lazy-load-image-component"; 7 | 8 | import { TextField, CardMedia, Alert, Fade } from "@mui/material"; 9 | import { LoadingButton } from "@mui/lab"; 10 | 11 | import { useFormik } from "formik"; 12 | import * as Yup from "yup"; 13 | 14 | export default function PickImage({ 15 | open, 16 | handleClose, 17 | setThumbnail, 18 | inputs, 19 | }: { 20 | open: boolean; 21 | handleClose: () => void; 22 | setThumbnail: (value: { src: string; alt: string }) => void; 23 | inputs?: { src: string; alt: string }; 24 | }) { 25 | const [alert, setAlert] = React.useState(false); 26 | 27 | // eslint-disable-next-line no-unused-vars 28 | const formik = useFormik<{ src: string; alt: string }>({ 29 | initialValues: inputs 30 | ? inputs 31 | : { 32 | src: "", 33 | alt: "", 34 | }, 35 | 36 | validationSchema: Yup.object().shape({ 37 | src: Yup.string().url().required(), 38 | alt: Yup.string().required(), 39 | }), 40 | onSubmit: () => {}, 41 | }); 42 | 43 | const { 44 | errors, 45 | touched, 46 | handleSubmit, 47 | isSubmitting, 48 | getFieldProps, 49 | values, 50 | setFieldValue, 51 | } = formik; 52 | return ( 53 | 61 | Insert Image 62 | 63 | 64 | 65 | Please fill the input. 66 | 67 | 68 | 74 | 93 | 94 | 113 | 114 | 115 | { 117 | // setThumbnail() 118 | 119 | if (formik.touched.src && !formik.errors["src"]) { 120 | setThumbnail(formik.values); 121 | handleClose(); 122 | } 123 | setAlert(true); 124 | }} 125 | > 126 | Save 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /components/renderers/VideoRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Dialog from "@mui/material/Dialog"; 3 | import DialogActions from "@mui/material/DialogActions"; 4 | import DialogContent from "@mui/material/DialogContent"; 5 | import DialogTitle from "@mui/material/DialogTitle"; 6 | 7 | import Accordion from "@mui/material/Accordion"; 8 | import AccordionDetails from "@mui/material/AccordionDetails"; 9 | import AccordionSummary from "@mui/material/AccordionSummary"; 10 | import Typography from "@mui/material/Typography"; 11 | import Divider from "@mui/material/Divider"; 12 | 13 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; 14 | 15 | import { Player, Video, DefaultUi } from "@vime/react"; 16 | 17 | import { TextField, CardMedia, Alert, Fade } from "@mui/material"; 18 | import { LoadingButton } from "@mui/lab"; 19 | 20 | import { useFormik } from "formik"; 21 | import * as Yup from "yup"; 22 | 23 | function getId(url: string) { 24 | const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; 25 | const match = url.match(regExp); 26 | 27 | return match && match[2].length === 11 ? match[2] : null; 28 | } 29 | 30 | export default function PickImage({ 31 | open, 32 | handleClose, 33 | setThumbnail, 34 | inputs, 35 | }: { 36 | open: boolean; 37 | handleClose: () => void; 38 | setThumbnail: (value: { 39 | src: string; 40 | poster: string; 41 | subtitles: Array<{ label: string; srcLang: string; src: string }>; 42 | }) => void; 43 | inputs?: { 44 | src: string; 45 | poster: string; 46 | subtitles: Array<{ label: string; srcLang: string; src: string }>; 47 | }; 48 | }) { 49 | const [expanded, setExpanded] = React.useState(false); 50 | 51 | const handleChange = 52 | (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { 53 | setExpanded(isExpanded ? panel : false); 54 | }; 55 | 56 | const [alert, setAlert] = React.useState(false); 57 | 58 | // eslint-disable-next-line no-unused-vars 59 | const formik = useFormik<{ 60 | src: string; 61 | poster: string; 62 | subtitles: Array<{ label: string; srcLang: string; src: string }>; 63 | }>({ 64 | initialValues: inputs 65 | ? inputs 66 | : { 67 | src: "", 68 | poster: "", 69 | subtitles: [], 70 | }, 71 | 72 | validationSchema: Yup.object().shape({ 73 | src: Yup.string().url().required(), 74 | poster: Yup.string().required(), 75 | subtitles: Yup.array().of(Yup.object()).required(), 76 | }), 77 | onSubmit: () => {}, 78 | }); 79 | 80 | const { 81 | errors, 82 | touched, 83 | handleSubmit, 84 | isSubmitting, 85 | getFieldProps, 86 | values, 87 | setFieldValue, 88 | } = formik; 89 | 90 | return ( 91 | 99 | Upload Video 100 | 101 | 102 | 103 | Please fill the input. 104 | 105 | 106 | 107 | {getId(values.src) ? ( 108 | 117 | ) : ( 118 | 119 | 122 | 123 | {/* We've replaced the `` component. */} 124 | {/* We can turn off any features we don't want via properties. */} 125 | 126 | {/* We can place our own UI components here to extend the default UI. */} 127 | 128 | 129 | )} 130 | 131 | 150 | 151 | 170 | 171 | {/* */} 172 | {/* 176 | } 178 | aria-controls="panel1bh-content" 179 | id="panel1bh-header" 180 | > 181 | 182 | Subtitle settings 183 | 184 | 185 | set subtitles on your video! 186 | 187 | 188 | 189 | 190 | Nulla facilisi. Phasellus sollicitudin nulla et quam mattis 191 | feugiat. Aliquam eget maximus est, id dignissim quam. 192 | 193 | 194 | */} 195 | 196 | 197 | { 199 | // setThumbnail() 200 | 201 | if (formik.touched.src && !formik.errors["src"]) { 202 | setThumbnail(formik.values); 203 | handleClose(); 204 | } 205 | setAlert(true); 206 | }} 207 | > 208 | Save 209 | 210 | 211 | 212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /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 | module.exports = { 3 | reactStrictMode: true, 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiptap-react-editor", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.5.0", 12 | "@emotion/styled": "^11.3.0", 13 | "@mui/icons-material": "^5.0.5", 14 | "@mui/lab": "^5.0.0-alpha.53", 15 | "@mui/material": "^5.0.6", 16 | "@mui/styles": "^5.0.2", 17 | "@tiptap/extension-character-count": "^2.0.0-beta.13", 18 | "@tiptap/extension-code-block-lowlight": "^2.0.0-beta.47", 19 | "@tiptap/extension-document": "^2.0.0-beta.13", 20 | "@tiptap/extension-dropcursor": "^2.0.0-beta.19", 21 | "@tiptap/extension-focus": "^2.0.0-beta.30", 22 | "@tiptap/extension-highlight": "^2.0.0-beta.25", 23 | "@tiptap/extension-link": "^2.0.0-beta.23", 24 | "@tiptap/extension-paragraph": "^2.0.0-beta.17", 25 | "@tiptap/extension-subscript": "^2.0.0-beta.5", 26 | "@tiptap/extension-superscript": "^2.0.0-beta.5", 27 | "@tiptap/extension-text": "^2.0.0-beta.13", 28 | "@tiptap/extension-text-align": "^2.0.0-beta.23", 29 | "@tiptap/extension-typography": "^2.0.0-beta.17", 30 | "@tiptap/extension-underline": "^2.0.0-beta.16", 31 | "@tiptap/react": "^2.0.0-beta.84", 32 | "@tiptap/starter-kit": "^2.0.0-beta.129", 33 | "@vime/core": "^5.0.34", 34 | "@vime/react": "5.0.31", 35 | "formik": "^2.2.9", 36 | "lodash": "^4.17.21", 37 | "next": "^12.0.9", 38 | "react": "17.0.2", 39 | "react-dom": "17.0.2", 40 | "react-lazy-load-image-component": "^1.5.1", 41 | "yup": "^0.32.11" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "16.11.6", 45 | "@types/react": "^17.0.33", 46 | "@types/react-lazy-load-image-component": "^1.5.2", 47 | "eslint": "7.32.0", 48 | "eslint-config-next": "12.0.2", 49 | "typescript": "4.4.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import ThemeConfig from "../theme"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return 7 | } 8 | 9 | export default MyApp 10 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Image from "next/image"; 4 | import { Container } from "@mui/material"; 5 | import styles from "../styles/Home.module.css"; 6 | import Editor from "components/content/index"; 7 | 8 | const Home: NextPage = () => { 9 | return ( 10 | 11 | 12 | TipTap Reactjs Example Editor 13 | 17 | 18 | 19 | 20 |
21 |

22 | Welcome to{" "} 23 | 24 | TipTap Example React Editor 25 | 26 |

27 | 28 |
29 | 30 | 39 |
40 | ); 41 | }; 42 | 43 | export default Home; 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldhanekaa/TipTap-Example-React-Editor/7cff6c89d555f3b6380bade20041b05825fb25dd/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | padding: 10rem 0; 7 | flex: 1; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | 14 | .footer { 15 | display: flex; 16 | flex: 1; 17 | padding: 2rem 0; 18 | border-top: 1px solid #eaeaea; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | 23 | .footer a { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | flex-grow: 1; 28 | } 29 | 30 | .title a { 31 | color: #00ab55; 32 | text-decoration: none; 33 | } 34 | 35 | .title a:hover, 36 | .title a:focus, 37 | .title a:active { 38 | text-decoration: underline; 39 | } 40 | 41 | .title { 42 | margin: 0; 43 | line-height: 1.15; 44 | font-size: 4rem; 45 | margin-bottom: 50px; 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 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /theme/breakpoints.js: -------------------------------------------------------------------------------- 1 | // THIS IS THE DEFAULT VALUE YOU CAN CHANGE IF YOU WANT 2 | 3 | const breakpoints = { 4 | values: { 5 | xs: 0, 6 | sm: 600, 7 | md: 960, 8 | lg: 1280, 9 | xl: 1920, 10 | }, 11 | }; 12 | 13 | export default breakpoints; 14 | -------------------------------------------------------------------------------- /theme/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { withStyles } from "@mui/styles"; 2 | import palette from "./palette"; 3 | // ---------------------------------------------------------------------- 4 | 5 | const GlobalStyles = withStyles(() => ({ 6 | "@global": { 7 | "*": { 8 | margin: 0, 9 | padding: 0, 10 | boxSizing: "border-box", 11 | }, 12 | html: { 13 | width: "100%", 14 | height: "100%", 15 | "-ms-text-size-adjust": "100%", 16 | "-webkit-overflow-scrolling": "touch", 17 | }, 18 | body: { 19 | width: "100%", 20 | height: "100%", 21 | }, 22 | "#root": { 23 | width: "100%", 24 | height: "100%", 25 | }, 26 | input: { 27 | "&[type=number]": { 28 | MozAppearance: "textfield", 29 | "&::-webkit-outer-spin-button": { margin: 0, WebkitAppearance: "none" }, 30 | "&::-webkit-inner-spin-button": { margin: 0, WebkitAppearance: "none" }, 31 | }, 32 | }, 33 | textarea: { 34 | "&::-webkit-input-placeholder": { color: palette.text.disabled }, 35 | "&::-moz-placeholder": { opacity: 1, color: palette.text.disabled }, 36 | "&:-ms-input-placeholder": { color: palette.text.disabled }, 37 | "&::placeholder": { color: palette.text.disabled }, 38 | }, 39 | a: { color: palette.primary.main }, 40 | img: { display: "block", maxWidth: "100%" }, 41 | }, 42 | }))(() => null); 43 | 44 | export default GlobalStyles; 45 | -------------------------------------------------------------------------------- /theme/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | // material 3 | import { CssBaseline } from "@mui/material"; 4 | import { 5 | ThemeProvider, 6 | StyledEngineProvider as StylesProvider, 7 | createTheme, 8 | } from "@mui/material/styles"; 9 | // 10 | import shape from "./shape"; 11 | import palette from "./palette"; 12 | import typography from "./typography"; 13 | import breakpoints from "./breakpoints"; 14 | // eslint-disable-next-line import/no-cycle 15 | import GlobalStyles from "./globalStyles"; 16 | import componentsOverride from "./overrides"; 17 | import shadows, { customShadows } from "./shadows"; 18 | 19 | // ---------------------------------------------------------------------- 20 | 21 | const theme = createTheme({ 22 | palette, 23 | shape, 24 | typography, 25 | breakpoints, 26 | shadows, 27 | customShadows, 28 | }); 29 | 30 | export { theme }; 31 | export default function ThemeConfig({ children }) { 32 | // const themeOptions = useMemo( 33 | // () => ({ 34 | // palette, 35 | // shape, 36 | // typography, 37 | // breakpoints, 38 | // shadows, 39 | // customShadows, 40 | // }), 41 | // [], 42 | // ); 43 | 44 | theme.components = componentsOverride(theme); 45 | return ( 46 | 47 | 48 | 49 | 50 | {children} 51 | 52 | 53 | ); 54 | } 55 | 56 | ThemeConfig.propTypes = { 57 | // eslint-disable-next-line react/require-default-props 58 | children: PropTypes.node, 59 | }; 60 | -------------------------------------------------------------------------------- /theme/overrides/Autocomplete.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Autocomplete(theme) { 4 | return { 5 | MuiAutocomplete: { 6 | styleOverrides: { 7 | paper: { 8 | boxShadow: theme.customShadows.z20 9 | } 10 | } 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /theme/overrides/Backdrop.js: -------------------------------------------------------------------------------- 1 | import { alpha } from "@mui/material/styles"; 2 | 3 | // ---------------------------------------------------------------------- 4 | 5 | export default function Backdrop(theme) { 6 | const varLow = alpha(theme.palette.grey[900], 0.48); 7 | const varHigh = alpha(theme.palette.grey[900], 1); 8 | 9 | return { 10 | MuiBackdrop: { 11 | styleOverrides: { 12 | root: { 13 | background: [ 14 | `rgb(22,28,36)`, 15 | `-moz-linear-gradient(75deg, ${varLow} 0%, ${varHigh} 100%)`, 16 | `-webkit-linear-gradient(75deg, ${varLow} 0%, ${varHigh} 100%)`, 17 | `linear-gradient(75deg, ${varLow} 0%, ${varHigh} 100%)`, 18 | ], 19 | "&.MuiBackdrop-invisible": { 20 | background: "transparent", 21 | }, 22 | }, 23 | }, 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /theme/overrides/Button.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Button(theme) { 4 | return { 5 | MuiButton: { 6 | styleOverrides: { 7 | root: { 8 | '&:hover': { 9 | boxShadow: 'none' 10 | } 11 | }, 12 | sizeLarge: { 13 | height: 48 14 | }, 15 | containedInherit: { 16 | color: theme.palette.grey[800], 17 | boxShadow: theme.customShadows.z8, 18 | '&:hover': { 19 | backgroundColor: theme.palette.grey[400] 20 | } 21 | }, 22 | containedPrimary: { 23 | boxShadow: theme.customShadows.primary 24 | }, 25 | containedSecondary: { 26 | boxShadow: theme.customShadows.secondary 27 | }, 28 | outlinedInherit: { 29 | border: `1px solid ${theme.palette.grey[500_32]}`, 30 | '&:hover': { 31 | backgroundColor: theme.palette.action.hover 32 | } 33 | }, 34 | textInherit: { 35 | '&:hover': { 36 | backgroundColor: theme.palette.action.hover 37 | } 38 | } 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /theme/overrides/Card.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Card(theme) { 4 | return { 5 | MuiCard: { 6 | styleOverrides: { 7 | root: { 8 | boxShadow: theme.customShadows.z16, 9 | borderRadius: theme.shape.borderRadiusMd, 10 | position: "relative", 11 | zIndex: 0, // Fix Safari overflow: hidden with border radius 12 | }, 13 | }, 14 | }, 15 | MuiCardHeader: { 16 | defaultProps: { 17 | titleTypographyProps: { variant: "h6" }, 18 | subheaderTypographyProps: { variant: "body2" }, 19 | }, 20 | styleOverrides: { 21 | root: { 22 | padding: 3, 23 | }, 24 | }, 25 | }, 26 | MuiCardContent: { 27 | styleOverrides: { 28 | root: { 29 | padding: 3, 30 | }, 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /theme/overrides/IconButton.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function IconButton(theme) { 4 | return { 5 | MuiIconButton: { 6 | variants: [ 7 | { 8 | props: { color: 'default' }, 9 | style: { 10 | '&:hover': { backgroundColor: theme.palette.action.hover } 11 | } 12 | }, 13 | { 14 | props: { color: 'inherit' }, 15 | style: { 16 | '&:hover': { backgroundColor: theme.palette.action.hover } 17 | } 18 | } 19 | ], 20 | 21 | styleOverrides: { 22 | root: {} 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /theme/overrides/Input.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Input(theme) { 4 | return { 5 | MuiInputBase: { 6 | styleOverrides: { 7 | root: { 8 | '&.Mui-disabled': { 9 | '& svg': { color: theme.palette.text.disabled } 10 | } 11 | }, 12 | input: { 13 | '&::placeholder': { 14 | opacity: 1, 15 | color: theme.palette.text.disabled 16 | } 17 | } 18 | } 19 | }, 20 | MuiInput: { 21 | styleOverrides: { 22 | underline: { 23 | '&:before': { 24 | borderBottomColor: theme.palette.grey[500_56] 25 | } 26 | } 27 | } 28 | }, 29 | MuiFilledInput: { 30 | styleOverrides: { 31 | root: { 32 | backgroundColor: theme.palette.grey[500_12], 33 | '&:hover': { 34 | backgroundColor: theme.palette.grey[500_16] 35 | }, 36 | '&.Mui-focused': { 37 | backgroundColor: theme.palette.action.focus 38 | }, 39 | '&.Mui-disabled': { 40 | backgroundColor: theme.palette.action.disabledBackground 41 | } 42 | }, 43 | underline: { 44 | '&:before': { 45 | borderBottomColor: theme.palette.grey[500_56] 46 | } 47 | } 48 | } 49 | }, 50 | MuiOutlinedInput: { 51 | styleOverrides: { 52 | root: { 53 | '& .MuiOutlinedInput-notchedOutline': { 54 | borderColor: theme.palette.grey[500_32] 55 | }, 56 | '&.Mui-disabled': { 57 | '& .MuiOutlinedInput-notchedOutline': { 58 | borderColor: theme.palette.action.disabledBackground 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /theme/overrides/Lists.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Lists(theme) { 4 | return { 5 | MuiListItemIcon: { 6 | styleOverrides: { 7 | root: { 8 | color: 'inherit', 9 | minWidth: 'auto', 10 | marginRight: 2, 11 | }, 12 | }, 13 | }, 14 | MuiListItemAvatar: { 15 | styleOverrides: { 16 | root: { 17 | minWidth: 'auto', 18 | marginRight: 2, 19 | }, 20 | }, 21 | }, 22 | MuiListItemText: { 23 | styleOverrides: { 24 | root: { 25 | marginTop: 0, 26 | marginBottom: 0, 27 | }, 28 | multiline: { 29 | marginTop: 0, 30 | marginBottom: 0, 31 | }, 32 | }, 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /theme/overrides/Paper.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Paper() { 4 | return { 5 | MuiPaper: { 6 | defaultProps: { 7 | elevation: 0 8 | }, 9 | 10 | styleOverrides: { 11 | root: { 12 | backgroundImage: 'none' 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /theme/overrides/Tooltip.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Tooltip(theme) { 4 | return { 5 | MuiTooltip: { 6 | styleOverrides: { 7 | tooltip: { 8 | backgroundColor: theme.palette.grey[800] 9 | }, 10 | arrow: { 11 | color: theme.palette.grey[800] 12 | } 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /theme/overrides/Typography.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | export default function Typography(theme) { 4 | return { 5 | MuiTypography: { 6 | styleOverrides: { 7 | paragraph: { 8 | marginBottom: 2, 9 | }, 10 | gutterBottom: { 11 | marginBottom: 1, 12 | }, 13 | }, 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /theme/overrides/index.js: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash"; 2 | import Card from "./Card"; 3 | import Lists from "./Lists"; 4 | import Paper from "./Paper"; 5 | import Input from "./Input"; 6 | import Button from "./Button"; 7 | import Tooltip from "./Tooltip"; 8 | import Backdrop from "./Backdrop"; 9 | import Typography from "./Typography"; 10 | import IconButton from "./IconButton"; 11 | import Autocomplete from "./Autocomplete"; 12 | 13 | // ---------------------------------------------------------------------- 14 | 15 | export default function ComponentsOverrides(theme) { 16 | return merge( 17 | Card(theme), 18 | Lists(theme), 19 | Paper(theme), 20 | Input(theme), 21 | Button(theme), 22 | Tooltip(theme), 23 | Backdrop(theme), 24 | Typography(theme), 25 | IconButton(theme), 26 | Autocomplete(theme) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /theme/palette.js: -------------------------------------------------------------------------------- 1 | import { alpha } from "@mui/material/styles"; 2 | 3 | // ---------------------------------------------------------------------- 4 | 5 | function createGradient(color1, color2) { 6 | return `linear-gradient(to bottom, ${color1}, ${color2})`; 7 | } 8 | 9 | // SETUP COLORS 10 | const GREY = { 11 | 0: "#FFFFFF", 12 | 100: "#F9FAFB", 13 | 200: "#F4F6F8", 14 | 300: "#DFE3E8", 15 | 400: "#C4CDD5", 16 | 500: "#919EAB", 17 | 600: "#637381", 18 | 700: "#454F5B", 19 | 800: "#212B36", 20 | 900: "#161C24", 21 | 500_8: alpha("#919EAB", 0.08), 22 | 500_12: alpha("#919EAB", 0.12), 23 | 500_16: alpha("#919EAB", 0.16), 24 | 500_24: alpha("#919EAB", 0.24), 25 | 500_32: alpha("#919EAB", 0.32), 26 | 500_48: alpha("#919EAB", 0.48), 27 | 500_56: alpha("#919EAB", 0.56), 28 | 500_80: alpha("#919EAB", 0.8), 29 | }; 30 | 31 | const PRIMARY = { 32 | lighter: "#C8FACD", 33 | light: "#5BE584", 34 | main: "#00AB55", 35 | dark: "#007B55", 36 | darker: "#005249", 37 | contrastText: "#fff", 38 | }; 39 | const SECONDARY = { 40 | lighter: "#D6E4FF", 41 | light: "#84A9FF", 42 | main: "#3366FF", 43 | dark: "#1939B7", 44 | darker: "#091A7A", 45 | contrastText: "#fff", 46 | }; 47 | const INFO = { 48 | lighter: "#D0F2FF", 49 | light: "#74CAFF", 50 | main: "#1890FF", 51 | dark: "#0C53B7", 52 | darker: "#04297A", 53 | contrastText: "#fff", 54 | }; 55 | const SUCCESS = { 56 | lighter: "#E9FCD4", 57 | light: "#AAF27F", 58 | main: "#54D62C", 59 | dark: "#229A16", 60 | darker: "#08660D", 61 | contrastText: GREY[800], 62 | }; 63 | const WARNING = { 64 | lighter: "#FFF7CD", 65 | light: "#FFE16A", 66 | main: "#FFC107", 67 | dark: "#B78103", 68 | darker: "#7A4F01", 69 | contrastText: GREY[800], 70 | }; 71 | const ERROR = { 72 | lighter: "#FFE7D9", 73 | light: "#FFA48D", 74 | main: "#FF4842", 75 | dark: "#B72136", 76 | darker: "#7A0C2E", 77 | contrastText: "#fff", 78 | }; 79 | 80 | const GRADIENTS = { 81 | primary: createGradient(PRIMARY.light, PRIMARY.main), 82 | info: createGradient(INFO.light, INFO.main), 83 | success: createGradient(SUCCESS.light, SUCCESS.main), 84 | warning: createGradient(WARNING.light, WARNING.main), 85 | error: createGradient(ERROR.light, ERROR.main), 86 | }; 87 | 88 | const palette = { 89 | common: { black: "#000", white: "#fff" }, 90 | primary: { ...PRIMARY }, 91 | secondary: { ...SECONDARY }, 92 | info: { ...INFO }, 93 | success: { ...SUCCESS }, 94 | warning: { ...WARNING }, 95 | error: { ...ERROR }, 96 | grey: GREY, 97 | gradients: GRADIENTS, 98 | divider: GREY[500_24], 99 | text: { primary: GREY[800], secondary: GREY[600], disabled: GREY[500] }, 100 | background: { paper: "#fff", default: "#fff", neutral: GREY[200] }, 101 | action: { 102 | active: GREY[600], 103 | hover: GREY[500_8], 104 | selected: GREY[500_16], 105 | disabled: GREY[500_80], 106 | disabledBackground: GREY[500_24], 107 | focus: GREY[500_24], 108 | hoverOpacity: 0.08, 109 | disabledOpacity: 0.48, 110 | }, 111 | }; 112 | 113 | export default palette; 114 | -------------------------------------------------------------------------------- /theme/shadows.js: -------------------------------------------------------------------------------- 1 | // material 2 | import { alpha } from "@mui/material/styles/"; 3 | import palette from "./palette"; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | const LIGHT_MODE = palette.grey[500]; 8 | 9 | const createShadow = (color) => { 10 | const transparent1 = alpha(color, 0.2); 11 | const transparent2 = alpha(color, 0.14); 12 | const transparent3 = alpha(color, 0.12); 13 | return [ 14 | "none", 15 | `0px 2px 1px -1px ${transparent1},0px 1px 1px 0px ${transparent2},0px 1px 3px 0px ${transparent3}`, 16 | `0px 3px 1px -2px ${transparent1},0px 2px 2px 0px ${transparent2},0px 1px 5px 0px ${transparent3}`, 17 | `0px 3px 3px -2px ${transparent1},0px 3px 4px 0px ${transparent2},0px 1px 8px 0px ${transparent3}`, 18 | `0px 2px 4px -1px ${transparent1},0px 4px 5px 0px ${transparent2},0px 1px 10px 0px ${transparent3}`, 19 | `0px 3px 5px -1px ${transparent1},0px 5px 8px 0px ${transparent2},0px 1px 14px 0px ${transparent3}`, 20 | `0px 3px 5px -1px ${transparent1},0px 6px 10px 0px ${transparent2},0px 1px 18px 0px ${transparent3}`, 21 | `0px 4px 5px -2px ${transparent1},0px 7px 10px 1px ${transparent2},0px 2px 16px 1px ${transparent3}`, 22 | `0px 5px 5px -3px ${transparent1},0px 8px 10px 1px ${transparent2},0px 3px 14px 2px ${transparent3}`, 23 | `0px 5px 6px -3px ${transparent1},0px 9px 12px 1px ${transparent2},0px 3px 16px 2px ${transparent3}`, 24 | `0px 6px 6px -3px ${transparent1},0px 10px 14px 1px ${transparent2},0px 4px 18px 3px ${transparent3}`, 25 | `0px 6px 7px -4px ${transparent1},0px 11px 15px 1px ${transparent2},0px 4px 20px 3px ${transparent3}`, 26 | `0px 7px 8px -4px ${transparent1},0px 12px 17px 2px ${transparent2},0px 5px 22px 4px ${transparent3}`, 27 | `0px 7px 8px -4px ${transparent1},0px 13px 19px 2px ${transparent2},0px 5px 24px 4px ${transparent3}`, 28 | `0px 7px 9px -4px ${transparent1},0px 14px 21px 2px ${transparent2},0px 5px 26px 4px ${transparent3}`, 29 | `0px 8px 9px -5px ${transparent1},0px 15px 22px 2px ${transparent2},0px 6px 28px 5px ${transparent3}`, 30 | `0px 8px 10px -5px ${transparent1},0px 16px 24px 2px ${transparent2},0px 6px 30px 5px ${transparent3}`, 31 | `0px 8px 11px -5px ${transparent1},0px 17px 26px 2px ${transparent2},0px 6px 32px 5px ${transparent3}`, 32 | `0px 9px 11px -5px ${transparent1},0px 18px 28px 2px ${transparent2},0px 7px 34px 6px ${transparent3}`, 33 | `0px 9px 12px -6px ${transparent1},0px 19px 29px 2px ${transparent2},0px 7px 36px 6px ${transparent3}`, 34 | `0px 10px 13px -6px ${transparent1},0px 20px 31px 3px ${transparent2},0px 8px 38px 7px ${transparent3}`, 35 | `0px 10px 13px -6px ${transparent1},0px 21px 33px 3px ${transparent2},0px 8px 40px 7px ${transparent3}`, 36 | `0px 10px 14px -6px ${transparent1},0px 22px 35px 3px ${transparent2},0px 8px 42px 7px ${transparent3}`, 37 | `0px 11px 14px -7px ${transparent1},0px 23px 36px 3px ${transparent2},0px 9px 44px 8px ${transparent3}`, 38 | `0px 11px 15px -7px ${transparent1},0px 24px 38px 3px ${transparent2},0px 9px 46px 8px ${transparent3}`, 39 | ]; 40 | }; 41 | 42 | const createCustomShadow = (color) => { 43 | const transparent = alpha(color, 0.24); 44 | 45 | return { 46 | z1: `0 1px 2px 0 ${transparent}`, 47 | z8: `0 8px 16px 0 ${transparent}`, 48 | z12: `0 0 2px 0 ${transparent}, 0 12px 24px 0 ${transparent}`, 49 | z16: `0 0 2px 0 ${transparent}, 0 16px 32px -4px ${transparent}`, 50 | z20: `0 0 2px 0 ${transparent}, 0 20px 40px -4px ${transparent}`, 51 | z24: `0 0 4px 0 ${transparent}, 0 24px 48px 0 ${transparent}`, 52 | primary: `0 8px 16px 0 ${alpha(palette.primary.main, 0.24)}`, 53 | secondary: `0 8px 16px 0 ${alpha(palette.secondary.main, 0.24)}`, 54 | info: `0 8px 16px 0 ${alpha(palette.info.main, 0.24)}`, 55 | success: `0 8px 16px 0 ${alpha(palette.success.main, 0.24)}`, 56 | warning: `0 8px 16px 0 ${alpha(palette.warning.main, 0.24)}`, 57 | error: `0 8px 16px 0 ${alpha(palette.error.main, 0.24)}`, 58 | }; 59 | }; 60 | 61 | export const customShadows = createCustomShadow(LIGHT_MODE); 62 | 63 | const shadows = createShadow(LIGHT_MODE); 64 | 65 | export default shadows; 66 | -------------------------------------------------------------------------------- /theme/shape.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | const shape = { 4 | borderRadius: 8, 5 | borderRadiusSm: 12, 6 | borderRadiusMd: 16 7 | }; 8 | 9 | export default shape; 10 | -------------------------------------------------------------------------------- /theme/typography.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------- 2 | 3 | function pxToRem(value) { 4 | return `${value / 16}rem`; 5 | } 6 | 7 | function responsiveFontSizes({ sm, md, lg }) { 8 | return { 9 | '@media (min-width:600px)': { 10 | fontSize: pxToRem(sm) 11 | }, 12 | '@media (min-width:960px)': { 13 | fontSize: pxToRem(md) 14 | }, 15 | '@media (min-width:1280px)': { 16 | fontSize: pxToRem(lg) 17 | } 18 | }; 19 | } 20 | 21 | const FONT_PRIMARY = 'Public Sans, sans-serif'; 22 | 23 | const typography = { 24 | fontFamily: FONT_PRIMARY, 25 | fontWeightRegular: 400, 26 | fontWeightMedium: 600, 27 | fontWeightBold: 700, 28 | h1: { 29 | fontWeight: 700, 30 | lineHeight: 80 / 64, 31 | fontSize: pxToRem(40), 32 | ...responsiveFontSizes({ sm: 52, md: 58, lg: 64 }) 33 | }, 34 | h2: { 35 | fontWeight: 700, 36 | lineHeight: 64 / 48, 37 | fontSize: pxToRem(32), 38 | ...responsiveFontSizes({ sm: 40, md: 44, lg: 48 }) 39 | }, 40 | h3: { 41 | fontWeight: 700, 42 | lineHeight: 1.5, 43 | fontSize: pxToRem(24), 44 | ...responsiveFontSizes({ sm: 26, md: 30, lg: 32 }) 45 | }, 46 | h4: { 47 | fontWeight: 700, 48 | lineHeight: 1.5, 49 | fontSize: pxToRem(20), 50 | ...responsiveFontSizes({ sm: 20, md: 24, lg: 24 }) 51 | }, 52 | h5: { 53 | fontWeight: 700, 54 | lineHeight: 1.5, 55 | fontSize: pxToRem(18), 56 | ...responsiveFontSizes({ sm: 19, md: 20, lg: 20 }) 57 | }, 58 | h6: { 59 | fontWeight: 700, 60 | lineHeight: 28 / 18, 61 | fontSize: pxToRem(17), 62 | ...responsiveFontSizes({ sm: 18, md: 18, lg: 18 }) 63 | }, 64 | subtitle1: { 65 | fontWeight: 600, 66 | lineHeight: 1.5, 67 | fontSize: pxToRem(16) 68 | }, 69 | subtitle2: { 70 | fontWeight: 600, 71 | lineHeight: 22 / 14, 72 | fontSize: pxToRem(14) 73 | }, 74 | body1: { 75 | lineHeight: 1.5, 76 | fontSize: pxToRem(16) 77 | }, 78 | body2: { 79 | lineHeight: 22 / 14, 80 | fontSize: pxToRem(14) 81 | }, 82 | caption: { 83 | lineHeight: 1.5, 84 | fontSize: pxToRem(12) 85 | }, 86 | overline: { 87 | fontWeight: 700, 88 | lineHeight: 1.5, 89 | fontSize: pxToRem(12), 90 | letterSpacing: 1.1, 91 | textTransform: 'uppercase' 92 | }, 93 | button: { 94 | fontWeight: 700, 95 | lineHeight: 24 / 14, 96 | fontSize: pxToRem(14), 97 | textTransform: 'capitalize' 98 | } 99 | }; 100 | 101 | export default typography; 102 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "baseUrl": "./", // This must be specified if "paths" is. 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | --------------------------------------------------------------------------------