├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── Banner.js ├── CodeEditorBlock.js ├── Header.js ├── Heart.js ├── ToggleDarkMode.js ├── button │ ├── Button.js │ ├── ButtonAction.js │ └── ButtonNew.js ├── card │ ├── Card.js │ ├── CardList.js │ └── CardLoading.js ├── checkbox │ └── Checkbox.js ├── dropdown │ ├── Dropdown.js │ └── DropdownItem.js ├── filter │ └── Filter.js ├── firebase │ └── firebase-config.js ├── form │ ├── FormGroup.js │ └── FormRow.js ├── icons │ ├── IconCards.js │ ├── IconChat.js │ ├── IconEdit.js │ ├── IconEye.js │ ├── IconFacebook.js │ ├── IconFilters.js │ ├── IconGithub.js │ ├── IconHeart.js │ ├── IconPointer.js │ ├── IconQuestion.js │ ├── IconTiktok.js │ ├── IconTrash.js │ ├── IconUsers.js │ ├── IconWebsite.js │ └── index.js ├── image │ └── Avatar.js ├── input │ └── Input.js ├── label │ ├── Label.js │ └── LabelStatus.js ├── layout │ ├── LayoutDashboard.js │ ├── LayoutMain.js │ └── Sidebar.js ├── loader │ ├── Loader.js │ └── LoaderList.js ├── modal │ ├── ModalClose.js │ ├── ModalReject.js │ └── ModalViewCode.js ├── table │ └── Table.js ├── textarea │ └── Textarea.js └── toggle │ └── Toggle.js ├── constant └── global-constant.js ├── contexts └── auth-context.js ├── db.json ├── functions.md ├── hooks ├── useDarkMode.js ├── useFetchCards.js ├── useFetchFilter.js ├── useFetchMembers.js ├── useInputChange.js ├── useLocalStorage.js └── useToggle.js ├── jsconfig.json ├── modules ├── card │ ├── CardAction.js │ ├── CardAddNew.js │ ├── CardFilterDropdown.js │ ├── CardManage.js │ └── CardUpdate.js └── filter │ ├── FilterAddNew.js │ └── FilterManage.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.js ├── _app.js ├── _document.js ├── api │ └── hello.js ├── index.js ├── login.js ├── manage │ ├── cards.js │ ├── filters.js │ ├── new-card.js │ ├── new-filter.js │ ├── update-card.js │ ├── update-filter.js │ └── users.js └── signup.js ├── plan.md ├── postcss.config.js ├── public ├── 404.png ├── SFMonoLigaturized-Regular.ttf ├── favicon.ico ├── firacodevf.woff2 ├── intervar.woff2 ├── logo.png └── vercel.svg ├── store └── global-store.js ├── styles ├── Home.module.css ├── _editor.scss └── globals.scss ├── tailwind.config.js ├── utils ├── classNames.js └── copyToClipboard.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["babel-plugin-styled-components"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@next/next/no-img-element": "off", 5 | "react/no-unescaped-entities": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.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*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Thanks for visiting my personal project ❤️ If you like it please give me a star ⭐️. 2 | 3 | Checkout online project at [codingui.dev](https://codingui.dev). 4 | -------------------------------------------------------------------------------- /components/Banner.js: -------------------------------------------------------------------------------- 1 | import IconWebsite from "./icons/IconWebsite"; 2 | import IconTiktok from "./icons/IconTiktok"; 3 | import IconPointer from "./icons/IconPointer"; 4 | import { IconFacebook } from "./icons/IconFacebook"; 5 | import Link from "next/link"; 6 | import React from "react"; 7 | import { IconChat, IconGithub } from "./icons"; 8 | 9 | const Banner = () => { 10 | return ( 11 |
12 |
13 | 14 |
15 |

16 | Get your free UI components with just few click 17 |

18 |
19 | 25 | 26 | View on Github 27 | 28 | 34 | 35 | Contact me 36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | ); 68 | }; 69 | 70 | function SocialIcon({ href, children }) { 71 | return ( 72 | 78 | {children} 79 | 80 | ); 81 | } 82 | 83 | export default Banner; 84 | -------------------------------------------------------------------------------- /components/CodeEditorBlock.js: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import PropTypes from "prop-types"; 3 | 4 | const CodeEditor = dynamic( 5 | () => import("@uiw/react-textarea-code-editor").then((mod) => mod.default), 6 | { ssr: false } 7 | ); 8 | function CodeEditorBlock(props) { 9 | const { language = "css", placeholder = "", name, code, onChange } = props; 10 | return ( 11 |
12 | 20 |
21 | ); 22 | } 23 | CodeEditorBlock.propTypes = { 24 | language: PropTypes.oneOf(["css", "html", "javascript"]).isRequired, 25 | placeholder: PropTypes.string, 26 | code: PropTypes.string, 27 | onChange: PropTypes.func, 28 | name: PropTypes.string.isRequired, 29 | }; 30 | export default CodeEditorBlock; 31 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import { useAuth } from "contexts/auth-context"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | import Avatar from "./image/Avatar"; 6 | 7 | const Header = () => { 8 | const { userInfo } = useAuth(); 9 | return ( 10 |
11 |
12 | 13 | 14 |
15 | codingUI 21 |
22 | CodingUI 23 |
24 | 25 | {userInfo?.email && ( 26 | 27 | 28 | 29 |

30 | Hello, 31 | 32 | {userInfo?.username || userInfo?.fullname || "user"} 33 | 34 |

35 |
36 | 37 | )} 38 | {!userInfo?.email && ( 39 |
40 | 41 | 42 | Sign up 43 | 44 | 45 | 46 | 47 | Login 48 | 49 | 50 |
51 | )} 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default Header; 58 | -------------------------------------------------------------------------------- /components/Heart.js: -------------------------------------------------------------------------------- 1 | import { collection, doc, getDoc, setDoc, updateDoc } from "firebase/firestore"; 2 | import React, { useEffect } from "react"; 3 | import { toast } from "react-toastify"; 4 | import { db } from "./firebase/firebase-config"; 5 | 6 | const Heart = () => { 7 | const [likes, setLikes] = React.useState(0); 8 | const [liked, setLiked] = React.useState(false); 9 | useEffect(() => { 10 | const getLikes = async () => { 11 | const likesDoc = doc(db, "hearts", "count"); 12 | const likesSnapshot = await getDoc(likesDoc); 13 | if (likesSnapshot.exists()) { 14 | setLikes(likesSnapshot.data().count); 15 | } else { 16 | setLikes(0); 17 | } 18 | }; 19 | getLikes(); 20 | }, []); 21 | const handleClickHeart = () => { 22 | if (likes > 100_000_000) { 23 | toast.warn("The database have reached the maximum likes"); 24 | return; 25 | } 26 | setLiked(true); 27 | setLikes((likes) => likes + 1); 28 | const colRef = doc(db, "hearts", "count"); 29 | updateDoc(colRef, { count: likes + 1 }, { merge: true }); 30 | setTimeout(() => { 31 | setLiked(false); 32 | }, 500); 33 | }; 34 | return ( 35 |
36 | {likes} 37 | 38 |
44 | 50 | 55 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default Heart; 62 | -------------------------------------------------------------------------------- /components/ToggleDarkMode.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import useDarkMode from "hooks/useDarkMode"; 4 | 5 | const ToggleDarkMode = (props) => { 6 | const [darkMode, setDarkMode] = useDarkMode(); 7 | return ( 8 | 55 | ); 56 | }; 57 | 58 | ToggleDarkMode.propTypes = { 59 | on: PropTypes.bool.isRequired, 60 | onClick: PropTypes.func, 61 | }; 62 | 63 | export default ToggleDarkMode; 64 | -------------------------------------------------------------------------------- /components/button/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const Button = ({ children, className = "", type = "button", ...props }) => { 5 | const { isLoading, ...rest } = props; 6 | const child = isLoading ? ( 7 |
8 | ) : ( 9 | children 10 | ); 11 | return ( 12 | 20 | ); 21 | }; 22 | Button.propTypes = { 23 | className: PropTypes.string, 24 | children: PropTypes.node, 25 | type: PropTypes.string, 26 | isLoading: PropTypes.bool, 27 | }; 28 | 29 | export default Button; 30 | -------------------------------------------------------------------------------- /components/button/ButtonAction.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ButtonAction = (props) => { 4 | const { onClick = () => null, className = "", children } = props; 5 | return ( 6 | 12 | ); 13 | }; 14 | 15 | export default ButtonAction; 16 | -------------------------------------------------------------------------------- /components/button/ButtonNew.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | const ButtonNew = ({ href = "/" }) => { 5 | return ( 6 | 7 | 8 | 15 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default ButtonNew; 28 | -------------------------------------------------------------------------------- /components/card/Card.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from "react"; 2 | import styled from "styled-components"; 3 | import parse from "html-react-parser"; 4 | import copyToClipBoard from "../../utils/copyToClipboard"; 5 | import pretty from "pretty"; 6 | import cssbeautify from "cssbeautify"; 7 | import PropTypes from "prop-types"; 8 | import { IconEye, IconHeart } from "components/icons"; 9 | import { globalStore } from "store/global-store"; 10 | import shallow from "zustand/shallow"; 11 | const CardStyles = styled.div` 12 | ${(props) => props.css} 13 | `; 14 | 15 | const Card = (props) => { 16 | const { 17 | title, 18 | htmlCode, 19 | cssCode, 20 | filter, 21 | author = null, 22 | preview = false, 23 | } = props; 24 | const [htmlSourceCode, setHtmlSourceCode] = useState(htmlCode); 25 | const [cssSourceCode, setCssSourceCode] = useState(cssCode); 26 | 27 | useEffect(() => { 28 | setHtmlSourceCode(htmlCode); 29 | setCssSourceCode(cssCode); 30 | }, [htmlCode, cssCode, preview]); 31 | const { setIsShowCode, setHtmlCodeView, setCssCodeView } = globalStore( 32 | (state) => ({ 33 | setIsShowCode: state.setIsShowCode, 34 | setHtmlCodeView: state.setHtmlCodeView, 35 | setCssCodeView: state.setCssCodeView, 36 | }), 37 | shallow 38 | ); 39 | const handleViewCode = () => { 40 | setIsShowCode(true); 41 | setHtmlCodeView(pretty(htmlSourceCode, { ocd: true })); 42 | setCssCodeView( 43 | cssbeautify(cssSourceCode, { 44 | indent: ` `, 45 | autosemicolon: true, 46 | }) 47 | ); 48 | }; 49 | return ( 50 | <> 51 |
55 |
56 |

57 | {author && ( 58 | <> 59 | Credit: 60 | 61 | {author} 62 | 63 | 64 | )} 65 |

66 | {!preview && ( 67 |
68 | 69 | 70 | 71 | {/* 72 | 73 | */} 74 |
75 | )} 76 |
77 |
78 | 79 | {preview && } 80 | {htmlSourceCode && <>{parse(htmlSourceCode)}} 81 | 82 |
83 |
84 |

85 | {title} 86 |

87 | {!preview && ( 88 |
92 | 95 | copyToClipBoard( 96 | cssbeautify(cssSourceCode, { 97 | indent: ` `, 98 | autosemicolon: true, 99 | }) 100 | ) 101 | } 102 | > 103 | CSS 104 | 105 | 108 | copyToClipBoard(pretty(htmlSourceCode, { ocd: true })) 109 | } 110 | > 111 | HTML 112 | 113 |
114 | )} 115 |
116 |
117 | 118 | ); 119 | }; 120 | 121 | function ButtonAction({ children, onClick }) { 122 | return ( 123 | 129 | ); 130 | } 131 | 132 | function ButtonCopy({ children, onClick = () => {}, type = "html" }) { 133 | let bgClassName = 134 | type === "html" ? "hover:bg-blue-500" : "hover:bg-orange-500"; 135 | 136 | return ( 137 | 157 | ); 158 | } 159 | Card.propTypes = { 160 | title: PropTypes.string.isRequired, 161 | filter: PropTypes.string.isRequired, 162 | htmlCode: PropTypes.string, 163 | cssCode: PropTypes.string, 164 | preview: PropTypes.bool, 165 | }; 166 | export default memo(Card); 167 | -------------------------------------------------------------------------------- /components/card/CardList.js: -------------------------------------------------------------------------------- 1 | import { cardStatus } from "constant/global-constant"; 2 | import useFetchCards from "hooks/useFetchCards"; 3 | import React from "react"; 4 | import Card from "./Card"; 5 | import CardLoading from "./CardLoading"; 6 | 7 | const CardList = () => { 8 | const { cards, isLoading } = useFetchCards({ status: cardStatus.APPROVED }); 9 | console.log("CardList ~ isLoading", isLoading); 10 | if (isLoading) 11 | return ( 12 |
16 | {Array(6) 17 | .fill(0) 18 | .map((item, index) => ( 19 | 20 | ))} 21 |
22 | ); 23 | return ( 24 |
28 | {cards.map((card) => { 29 | return ( 30 | 38 | ); 39 | })} 40 |
41 | ); 42 | }; 43 | 44 | export default CardList; 45 | -------------------------------------------------------------------------------- /components/card/CardLoading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CardLoading = () => { 4 | return ( 5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |

22 |
26 |

27 |
31 |
35 |
39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | export default CardLoading; 46 | -------------------------------------------------------------------------------- /components/checkbox/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "utils/classNames"; 3 | 4 | const Checkbox = ({ checked = false, onClick = () => {} }) => { 5 | return ( 6 | 34 | ); 35 | }; 36 | 37 | export default Checkbox; 38 | -------------------------------------------------------------------------------- /components/dropdown/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const Dropdown = ({ 4 | placeholder = "Select filter", 5 | show = false, 6 | onClick = () => {}, 7 | children, 8 | }) => { 9 | const ref = React.useRef(null); 10 | const [showDropdown, setShowDropdown] = useState(show); 11 | useEffect(() => { 12 | function handleClickOutside(event) { 13 | if (ref.current && !ref.current.contains(event.target)) { 14 | setShowDropdown(false); 15 | } 16 | } 17 | window.addEventListener("click", handleClickOutside); 18 | return () => { 19 | window.removeEventListener("click", handleClickOutside); 20 | }; 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, []); 23 | const handleToggleDropdown = () => { 24 | setShowDropdown(!showDropdown); 25 | }; 26 | return ( 27 |
28 |
33 | {placeholder} 34 |
35 | {showDropdown && ( 36 |
37 | {children} 38 |
39 | )} 40 |
41 | ); 42 | }; 43 | 44 | export default Dropdown; 45 | -------------------------------------------------------------------------------- /components/dropdown/DropdownItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DropdownItem = ({ children, onClick = (v) => {} }) => { 4 | return ( 5 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export default DropdownItem; 15 | -------------------------------------------------------------------------------- /components/filter/Filter.js: -------------------------------------------------------------------------------- 1 | import useFetchFilter from "hooks/useFetchFilter"; 2 | import React from "react"; 3 | 4 | const FilterMenu = () => { 5 | const [activeCard, setActiveCard] = React.useState("all"); 6 | const { filters: filterItems } = useFetchFilter(true); 7 | const handleFilterCard = (item) => { 8 | setActiveCard(item); 9 | const cards = document.querySelectorAll(".card"); 10 | cards.forEach((card) => { 11 | card.style.display = 12 | card.getAttribute("data-filter") === item || item === "all" 13 | ? "flex" 14 | : "none"; 15 | }); 16 | }; 17 | return ( 18 |
22 | handleFilterCard("all")} 27 | activeCard={activeCard === "all"} 28 | > 29 | {filterItems.map((item) => ( 30 | handleFilterCard(item.name)} 33 | activeCard={activeCard === item.name} 34 | item={item} 35 | > 36 | ))} 37 |
38 | ); 39 | }; 40 | 41 | function FilterItem({ item, activeCard, onClick }) { 42 | return ( 43 |
52 | {item.name} 53 |
54 | ); 55 | } 56 | 57 | export default FilterMenu; 58 | -------------------------------------------------------------------------------- /components/firebase/firebase-config.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getFirestore } from "firebase/firestore"; 3 | import { getAuth } from "firebase/auth"; 4 | const firebaseConfig = { 5 | apiKey: "AIzaSyDez1vRc55klAJpMg_94VQ32UCn2brnt5k", 6 | authDomain: "coding-ui-3c8af.firebaseapp.com", 7 | projectId: "coding-ui-3c8af", 8 | storageBucket: "coding-ui-3c8af.appspot.com", 9 | messagingSenderId: "383245459906", 10 | appId: "1:383245459906:web:29604aef5f54935b88f65b", 11 | }; 12 | 13 | // Initialize Firebase 14 | const app = initializeApp(firebaseConfig); 15 | export const db = getFirestore(app); 16 | export const auth = getAuth(app); 17 | -------------------------------------------------------------------------------- /components/form/FormGroup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FormGroup = ({ children, className = "" }) => { 4 | return ( 5 |
8 | {children} 9 |
10 | ); 11 | }; 12 | 13 | export default FormGroup; 14 | -------------------------------------------------------------------------------- /components/form/FormRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FormRow = ({ children, className = "" }) => { 4 | return ( 5 |
{children}
6 | ); 7 | }; 8 | 9 | export default FormRow; 10 | -------------------------------------------------------------------------------- /components/icons/IconCards.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconCards({}) { 3 | return ( 4 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/icons/IconChat.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IconChat = () => { 4 | return ( 5 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default IconChat; 18 | -------------------------------------------------------------------------------- /components/icons/IconEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IconEdit = () => { 4 | return ( 5 | 11 | 12 | 17 | 18 | ); 19 | }; 20 | 21 | export default IconEdit; 22 | -------------------------------------------------------------------------------- /components/icons/IconEye.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IconEye = () => { 4 | return ( 5 | 11 | 12 | 17 | 18 | ); 19 | }; 20 | 21 | export default IconEye; 22 | -------------------------------------------------------------------------------- /components/icons/IconFacebook.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export function IconFacebook({}) { 3 | return ( 4 | 11 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/IconFilters.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconFilters({}) { 3 | return ( 4 | 10 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/icons/IconGithub.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IconGithub = () => { 4 | return ( 5 | 11 | 15 | 16 | ); 17 | }; 18 | 19 | export default IconGithub; 20 | -------------------------------------------------------------------------------- /components/icons/IconHeart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IconHeart = () => { 4 | return ( 5 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export default IconHeart; 21 | -------------------------------------------------------------------------------- /components/icons/IconPointer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconPointer({}) { 3 | return ( 4 | 11 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/IconQuestion.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconQuestion({}) { 3 | return ( 4 | 11 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/IconTiktok.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconTiktok({}) { 3 | return ( 4 | 11 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/IconTrash.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IconTrash = () => { 4 | return ( 5 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export default IconTrash; 21 | -------------------------------------------------------------------------------- /components/icons/IconUsers.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconUsers({}) { 3 | return ( 4 | 10 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/icons/IconWebsite.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function IconWebsite({}) { 3 | return ( 4 | 11 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/index.js: -------------------------------------------------------------------------------- 1 | export const { default: IconEdit } = require("./IconEdit"); 2 | export const { default: IconTrash } = require("./IconTrash"); 3 | export const { default: IconEye } = require("./IconEye"); 4 | export const { default: IconHeart } = require("./IconHeart"); 5 | export const { default: IconGithub } = require("./IconGithub"); 6 | export const { default: IconChat } = require("./IconChat"); 7 | -------------------------------------------------------------------------------- /components/image/Avatar.js: -------------------------------------------------------------------------------- 1 | import { useAuth } from "contexts/auth-context"; 2 | import React from "react"; 3 | import classNames from "utils/classNames"; 4 | 5 | const colors = ["#ffa400", "#fc6c8f", "#6a5af9", "#d66efd"]; 6 | function getRandomColor() { 7 | const index = Math.floor(Math.random() * colors.length); 8 | return colors[index]; 9 | } 10 | const Avatar = ({ className = "", name = "T" }) => { 11 | const { userInfo } = useAuth(); 12 | const newName = 13 | userInfo?.username?.split("")[0] || userInfo?.fullname?.split("")[0] || "C"; 14 | const color = getRandomColor(); 15 | return ( 16 |
25 | {newName} 26 |
27 | ); 28 | }; 29 | 30 | export default Avatar; 31 | -------------------------------------------------------------------------------- /components/input/Input.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "utils/classNames"; 3 | 4 | const Input = (props) => { 5 | const { type = "text", className = "", ...rest } = props; 6 | const [inputType, setInputType] = React.useState(type); 7 | function togglePassword() { 8 | setInputType(inputType === "password" ? "text" : "password"); 9 | } 10 | return ( 11 |
12 | 21 | {type === "password" && ( 22 | <> 23 | {inputType === "password" ? ( 24 | 32 | 38 | 44 | 45 | ) : ( 46 | 54 | 60 | 61 | )} 62 | 63 | )} 64 |
65 | ); 66 | }; 67 | 68 | export default Input; 69 | -------------------------------------------------------------------------------- /components/label/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Label = ({ children, className = "" }) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Label; 14 | -------------------------------------------------------------------------------- /components/label/LabelStatus.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LabelStatus = ({ children, className = "bg-green-500", ...rest }) => { 4 | return ( 5 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | export default LabelStatus; 15 | -------------------------------------------------------------------------------- /components/layout/LayoutDashboard.js: -------------------------------------------------------------------------------- 1 | import { useAuth } from "contexts/auth-context"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | import PageNotFound from "pages/404"; 6 | import React, { useEffect, useState } from "react"; 7 | import Sidebar from "./Sidebar"; 8 | 9 | const LayoutDashboard = ({ 10 | children, 11 | heading = "", 12 | hasPermission = false, 13 | back = "", 14 | }) => { 15 | const { userInfo, loading } = useAuth(); 16 | 17 | if (loading) return null; 18 | if (!userInfo?.email || !hasPermission) return ; 19 | return ( 20 |
21 | 22 | CodingUI - {heading} 23 | 24 | 25 |
26 |
27 | {back && } 28 |

29 | {heading} 30 |

31 |
32 |
{children}
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default LayoutDashboard; 39 | 40 | function GoBack({ back }) { 41 | return ( 42 | 43 | 44 | 51 | 57 | 58 | Back 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /components/layout/LayoutMain.js: -------------------------------------------------------------------------------- 1 | import Banner from "components/Banner"; 2 | import Header from "components/Header"; 3 | import Heart from "components/Heart"; 4 | import Head from "next/head"; 5 | import React from "react"; 6 | 7 | const LayoutMain = ({ children, title = "CodingUI", hideBanner = false }) => { 8 | return ( 9 | <> 10 | 11 | {title} 12 | 13 |
14 | 15 |
16 | {!hideBanner && } 17 | {children} 18 |
19 | 20 | ); 21 | }; 22 | 23 | export default LayoutMain; 24 | -------------------------------------------------------------------------------- /components/layout/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import classNames from "utils/classNames"; 4 | import { useRouter } from "next/router"; 5 | import { toast } from "react-toastify"; 6 | import { signOut } from "firebase/auth"; 7 | import { menus } from "constant/global-constant"; 8 | import { auth } from "components/firebase/firebase-config"; 9 | 10 | const Sidebar = () => { 11 | return ( 12 |
16 | 17 | 22 | 23 |
24 | ); 25 | }; 26 | function ButtonLogout() { 27 | const handleSignOut = () => { 28 | signOut(auth).then(() => { 29 | toast.success("Sign out successfully"); 30 | }); 31 | }; 32 | const IconLogout = ( 33 | 40 | 46 | 47 | ); 48 | return ( 49 |
50 | 57 |
58 | ); 59 | } 60 | function MenuItem({ menu }) { 61 | const router = useRouter(); 62 | const currentRoute = router.pathname; 63 | return ( 64 |
  • 65 | 66 | 72 | {menu.icon} 73 | {menu.title} 74 | 75 | 76 |
  • 77 | ); 78 | } 79 | 80 | function Logo({}) { 81 | return ( 82 | 83 | 84 | codingui 85 | CodingUI 86 | 87 | 88 | ); 89 | } 90 | export default Sidebar; 91 | -------------------------------------------------------------------------------- /components/loader/Loader.js: -------------------------------------------------------------------------------- 1 | import Card from "components/card/Card"; 2 | 3 | export const LoadingCircle = () => { 4 | return ( 5 | `} 9 | cssCode={ 10 | /*css*/ /*css*/ ` 11 | div { 12 | width: 50px; 13 | height: 50px; 14 | border-radius: 15rem; 15 | position: relative; 16 | margin: 0 auto; 17 | } 18 | div:before { 19 | content: ""; 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | width: 100%; 24 | height: 100%; 25 | border-radius: inherit; 26 | border: 4px solid transparent; 27 | border-right-color: #f62682; 28 | border-bottom-color: #f62682; 29 | animation: circleLoading 1s forwards infinite linear; 30 | } 31 | @keyframes circleLoading { 32 | to { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | ` 37 | } 38 | > 39 | ); 40 | }; 41 | export const LoadingBar = () => { 42 | return ( 43 | `} 47 | cssCode={ 48 | /*css*/ /*css*/ ` 49 | div { 50 | width: 100%; 51 | height: 4px; 52 | position: relative; 53 | margin: 0 auto; 54 | } 55 | div:before { 56 | content: ""; 57 | position: absolute; 58 | right: auto; 59 | left: 0; 60 | height: 100%; 61 | background-color: #f62682; 62 | animation: lineLoading 1s forwards infinite linear; 63 | } 64 | 65 | @keyframes lineLoading { 66 | 0% { 67 | right: 100%; 68 | } 69 | 50% { 70 | right: 0; 71 | left: 0; 72 | } 73 | 100% { 74 | right: 0; 75 | left: 100%; 76 | } 77 | } 78 | ` 79 | } 80 | > 81 | ); 82 | }; 83 | export const LoadingCircleDashed = () => { 84 | return ( 85 | `} 89 | cssCode={ 90 | /*css*/ /*css*/ ` 91 | .dashed-loading { 92 | position: relative; 93 | height: 50px; 94 | } 95 | .dashed-loading:after, 96 | .dashed-loading:before { 97 | content: ""; 98 | position: absolute; 99 | top: 0; 100 | left: 0; 101 | border-radius: 50%; 102 | width: 50px; 103 | height: 50px; 104 | } 105 | .dashed-loading:before { 106 | z-index: 5; 107 | border: 3px dashed #f62682; 108 | border-left: 3px solid transparent; 109 | border-bottom: 3px solid transparent; 110 | -webkit-animation: dashed 1s linear infinite; 111 | animation: dashed 1s linear infinite; 112 | } 113 | .dashed-loading:after { 114 | z-index: 10; 115 | border: 3px solid #f62682; 116 | border-left: 3px solid transparent; 117 | border-bottom: 3px solid transparent; 118 | -webkit-animation: dashed 1s ease infinite; 119 | animation: dashed 1s ease infinite; 120 | } 121 | @keyframes dashed { 122 | to { 123 | transform: rotate(360deg); 124 | } 125 | } 126 | ` 127 | } 128 | > 129 | ); 130 | }; 131 | export const LoadingFade = () => { 132 | return ( 133 | `} 137 | cssCode={ 138 | /*css*/ /*css*/ ` 139 | .fade-loading { 140 | width: 4rem; 141 | height: 4rem; 142 | background-color: #f62682; 143 | border-radius: 5rem; 144 | margin: 2rem auto; 145 | position: relative; 146 | } 147 | .fade-loading:before { 148 | content: ""; 149 | position: absolute; 150 | top: 0; 151 | left: 0; 152 | width: 100%; 153 | height: 100%; 154 | border-radius: inherit; 155 | background-color: inherit; 156 | animation: fade 1s forwards infinite linear; 157 | } 158 | @keyframes fade { 159 | to { 160 | transform: scale(2); 161 | opacity: 0; 162 | } 163 | } 164 | ` 165 | } 166 | > 167 | ); 168 | }; 169 | -------------------------------------------------------------------------------- /components/loader/LoaderList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LoadingBar, LoadingCircle, LoadingCircleDashed } from "./Loader"; 3 | 4 | const LoaderList = () => { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default LoaderList; 15 | -------------------------------------------------------------------------------- /components/modal/ModalClose.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ModalClose = ({ onClick = () => {} }) => { 4 | return ( 5 |
    10 | 17 | 23 | 24 |
    25 | ); 26 | }; 27 | 28 | export default ModalClose; 29 | -------------------------------------------------------------------------------- /components/modal/ModalReject.js: -------------------------------------------------------------------------------- 1 | import Label from "components/label/Label"; 2 | import React from "react"; 3 | import ReactModal from "react-modal"; 4 | import { globalStore } from "store/global-store"; 5 | import classNames from "utils/classNames"; 6 | import shallow from "zustand/shallow"; 7 | import ModalClose from "./ModalClose"; 8 | 9 | const ModalReject = () => { 10 | const { isShowReasonModal, setIsShowReasonModal, reason } = globalStore( 11 | (state) => ({ 12 | reason: state.reason, 13 | isShowReasonModal: state.isShowReasonModal, 14 | setIsShowReasonModal: state.setIsShowReasonModal, 15 | }), 16 | shallow 17 | ); 18 | return ( 19 | setIsShowReasonModal(false)} 24 | > 25 | setIsShowReasonModal(false)}> 26 |
    27 |
    28 | 29 |

    35 | {reason || "Good job 👍"} 36 |

    37 |
    38 |
    39 | ); 40 | }; 41 | 42 | export default ModalReject; 43 | -------------------------------------------------------------------------------- /components/modal/ModalViewCode.js: -------------------------------------------------------------------------------- 1 | import CodeEditorBlock from "components/CodeEditorBlock"; 2 | import Label from "components/label/Label"; 3 | import React from "react"; 4 | import ReactModal from "react-modal"; 5 | import { globalStore } from "store/global-store"; 6 | import shallow from "zustand/shallow"; 7 | import ModalClose from "./ModalClose"; 8 | 9 | const ModalViewCode = () => { 10 | const { isShowCode, setIsShowCode, htmlCodeView, cssCodeView } = globalStore( 11 | (state) => ({ 12 | isShowCode: state.isShowCode, 13 | setIsShowCode: state.setIsShowCode, 14 | htmlCodeView: state.htmlCodeView, 15 | cssCodeView: state.cssCodeView, 16 | }), 17 | shallow 18 | ); 19 | return ( 20 | setIsShowCode(false)} 25 | > 26 | setIsShowCode(false)}> 27 |
    28 |
    29 | 30 | 35 |
    36 |
    37 | 38 | 43 |
    44 |
    45 | ); 46 | }; 47 | 48 | export default ModalViewCode; 49 | -------------------------------------------------------------------------------- /components/table/Table.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Table = () => { 4 | return
    ; 5 | }; 6 | 7 | export default Table; 8 | -------------------------------------------------------------------------------- /components/textarea/Textarea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Textarea = (props) => { 4 | const { name, ...rest } = props; 5 | return ( 6 | 11 | ); 12 | }; 13 | 14 | export default Textarea; 15 | -------------------------------------------------------------------------------- /components/toggle/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Toggle = ({ onChange = () => {}, on = false, name = "status" }) => { 4 | return ( 5 | 25 | ); 26 | }; 27 | 28 | export default Toggle; 29 | -------------------------------------------------------------------------------- /constant/global-constant.js: -------------------------------------------------------------------------------- 1 | import IconFilters from "./../components/icons/IconFilters"; 2 | import IconUsers from "./../components/icons/IconUsers"; 3 | import IconCards from "./../components/icons/IconCards"; 4 | export const filterItems = [ 5 | "All", 6 | "Button", 7 | "Input", 8 | "Loading", 9 | "Toggle", 10 | "Dropdown", 11 | "Card", 12 | "Breadcrumbs", 13 | "Pagination", 14 | "Table", 15 | "Timeline", 16 | ]; 17 | export const menus = [ 18 | { 19 | title: "Dashboard", 20 | icon: , 21 | link: "/manage/cards", 22 | }, 23 | { 24 | title: "Members", 25 | icon: , 26 | link: "/manage/users", 27 | }, 28 | { 29 | title: "Filters", 30 | icon: , 31 | link: "/manage/filters", 32 | }, 33 | ]; 34 | export const cardStatus = { 35 | APPROVED: 1, 36 | PENDING: 2, 37 | REJECTED: 3, 38 | }; 39 | export const filterStatus = { 40 | APPROVED: true, 41 | REJECTED: false, 42 | }; 43 | export const userStatus = { 44 | ACTIVE: "ACTIVE", 45 | INACTIVE: "INACTIVE", 46 | }; 47 | export const userRole = { 48 | USER: "USER", 49 | MOD: "MOD", 50 | ADMIN: "ADMIN", 51 | }; 52 | export const DATA_PER_PAGE = 10; 53 | -------------------------------------------------------------------------------- /contexts/auth-context.js: -------------------------------------------------------------------------------- 1 | import { auth, db } from "components/firebase/firebase-config"; 2 | import { onAuthStateChanged } from "firebase/auth"; 3 | import { collection, onSnapshot, query, where } from "firebase/firestore"; 4 | 5 | const { createContext, useContext, useState, useEffect } = require("react"); 6 | 7 | const AuthContext = createContext(); 8 | function AuthProvider(props) { 9 | const [loading, setLoading] = useState(true); 10 | const [userInfo, setUserInfo] = useState({ 11 | email: "", 12 | }); 13 | const value = { userInfo, setUserInfo, loading }; 14 | useEffect(() => { 15 | onAuthStateChanged(auth, (user) => { 16 | if (user) { 17 | const docRef = query( 18 | collection(db, "users"), 19 | where("email", "==", user.email) 20 | ); 21 | onSnapshot(docRef, (snapshot) => { 22 | snapshot.forEach((doc) => { 23 | setUserInfo({ 24 | ...user, 25 | ...doc.data(), 26 | }); 27 | }); 28 | }); 29 | // setUserInfo(user); 30 | } else { 31 | setUserInfo(null); 32 | } 33 | setLoading(false); 34 | }); 35 | }, []); 36 | return ; 37 | } 38 | function useAuth() { 39 | const context = useContext(AuthContext); 40 | if (typeof context === "undefined") 41 | throw new Error("useAuth must be used within AuthProvider"); 42 | return context; 43 | } 44 | export { AuthProvider, useAuth }; 45 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "cards": [ 3 | { 4 | "title": "Loading circle", 5 | "filter": "Loading", 6 | "htmlCode": "
    ", 7 | "cssCode": "" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /functions.md: -------------------------------------------------------------------------------- 1 | - Add user `role`: Admin, Creator, User 2 | - Add liked functionality 3 | - Update profile page 4 | - Upload image avatar 5 | - Global settings for website 6 | - Add .env config 7 | -------------------------------------------------------------------------------- /hooks/useDarkMode.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import useLocalStorage from "./useLocalStorage"; 3 | 4 | export default function useDarkMode() { 5 | const [enabledState, setEnabledState] = useLocalStorage("dark-mode-enabled"); 6 | const enabled = enabledState; 7 | useEffect(() => { 8 | const className = "dark"; 9 | const element = document.documentElement; 10 | if (enabled) { 11 | element.classList.add(className); 12 | } else { 13 | element.classList.remove(className); 14 | } 15 | }, [enabled]); 16 | return [enabled, setEnabledState]; 17 | } 18 | -------------------------------------------------------------------------------- /hooks/useFetchCards.js: -------------------------------------------------------------------------------- 1 | import { db } from "components/firebase/firebase-config"; 2 | import { userRole } from "constant/global-constant"; 3 | import { useAuth } from "contexts/auth-context"; 4 | import { 5 | collection, 6 | getDocs, 7 | limit, 8 | onSnapshot, 9 | orderBy, 10 | query, 11 | startAfter, 12 | where, 13 | } from "firebase/firestore"; 14 | import { useEffect, useState } from "react"; 15 | import { toast } from "react-toastify"; 16 | 17 | export default function useFetchCards({ 18 | status = null, 19 | name = "", 20 | filter = "", 21 | count = 100, 22 | isManage = false, 23 | }) { 24 | const { userInfo } = useAuth(); 25 | const [loading, setLoading] = useState(true); 26 | const [cards, setCards] = useState([]); 27 | const [lastDoc, setLastDoc] = useState(); 28 | const [total, setTotal] = useState(0); 29 | 30 | useEffect(() => { 31 | async function fetchData() { 32 | try { 33 | setLoading(true); 34 | let colRef = collection(db, "cards"); 35 | if (isManage && userInfo?.role === userRole.USER) { 36 | colRef = query(colRef, where("userId", "==", userInfo.uid)); 37 | } 38 | let queries = query(colRef, orderBy("createdAt", "desc"), limit(count)); 39 | if (status) 40 | queries = query(queries, where("status", "==", status), limit(count)); 41 | if (name) 42 | queries = query( 43 | colRef, 44 | where("title", ">=", name), 45 | where("title", "<=", name + "utf8"), 46 | orderBy("title", "desc"), 47 | limit(count) 48 | ); 49 | if (filter) 50 | queries = query(queries, where("filter", "==", filter), limit(count)); 51 | const documentSnapshots = await getDocs(queries); 52 | const lastVisible = 53 | documentSnapshots.docs[documentSnapshots.docs.length - 1]; 54 | onSnapshot(colRef, (querySnapshot) => { 55 | setTotal(querySnapshot.size); 56 | }); 57 | onSnapshot(queries, (querySnapshot) => { 58 | const results = []; 59 | querySnapshot.forEach((doc) => { 60 | results.push({ id: doc.id, ...doc.data() }); 61 | }); 62 | setCards(results); 63 | }); 64 | setLastDoc(lastVisible); 65 | } catch (err) { 66 | console.log(err); 67 | toast.error(err?.message); 68 | } finally { 69 | setLoading(false); 70 | } 71 | } 72 | fetchData(); 73 | // eslint-disable-next-line react-hooks/exhaustive-deps 74 | }, [filter, name, status, count]); 75 | const handleLoadMore = async () => { 76 | setLoading(true); 77 | let colRef = collection(db, "cards"); 78 | if (isManage && userInfo?.role === userRole.USER) { 79 | colRef = query(colRef, where("userId", "==", userInfo.uid)); 80 | } 81 | let queries = query( 82 | colRef, 83 | orderBy("createdAt", "desc"), 84 | startAfter(lastDoc || 0), 85 | limit(count) 86 | ); 87 | if (status) 88 | queries = query( 89 | queries, 90 | startAfter(lastDoc || 0), 91 | where("status", "==", status), 92 | limit(count) 93 | ); 94 | if (name) 95 | queries = query( 96 | colRef, 97 | where("title", ">=", name), 98 | where("title", "<=", name + "utf8"), 99 | orderBy("title", "desc"), 100 | startAfter(lastDoc || 0), 101 | limit(count) 102 | ); 103 | if (filter) 104 | queries = query( 105 | queries, 106 | startAfter(lastDoc || 0), 107 | where("filter", "==", filter), 108 | limit(count) 109 | ); 110 | const documentSnapshots = await getDocs(queries); 111 | const lastVisible = 112 | documentSnapshots.docs[documentSnapshots.docs.length - 1]; 113 | setLastDoc(lastVisible); 114 | setLoading(false); 115 | onSnapshot(queries, (snapshot) => { 116 | let results = []; 117 | snapshot.forEach((doc) => { 118 | results.push({ 119 | id: doc.id, 120 | ...doc.data(), 121 | }); 122 | }); 123 | setCards([...cards, ...results]); 124 | }); 125 | }; 126 | return { 127 | total, 128 | cards, 129 | isLoading: loading, 130 | lastDoc, 131 | handleLoadMore, 132 | isReachingEnd: total < cards.length || total < count, 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /hooks/useFetchFilter.js: -------------------------------------------------------------------------------- 1 | import { db } from "components/firebase/firebase-config"; 2 | import { collection, onSnapshot, query, where } from "firebase/firestore"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export default function useFetchFilter(status = null) { 6 | const [filters, setFilters] = useState([]); 7 | useEffect(() => { 8 | async function fetchData() { 9 | let colRef = collection(db, "filters"); 10 | if (status) { 11 | colRef = query(colRef, where("status", "==", status)); 12 | } 13 | onSnapshot(colRef, (querySnapshot) => { 14 | const results = []; 15 | querySnapshot.forEach((doc) => { 16 | results.push({ id: doc.id, ...doc.data() }); 17 | }); 18 | setFilters(results); 19 | }); 20 | } 21 | fetchData(); 22 | }, [status]); 23 | return { 24 | filters, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /hooks/useFetchMembers.js: -------------------------------------------------------------------------------- 1 | import { db } from "components/firebase/firebase-config"; 2 | import { DATA_PER_PAGE } from "constant/global-constant"; 3 | import { 4 | collection, 5 | getDocs, 6 | limit, 7 | onSnapshot, 8 | query, 9 | startAfter, 10 | where, 11 | } from "firebase/firestore"; 12 | import { useEffect, useState } from "react"; 13 | 14 | export default function useFetchMembers({ 15 | status = null, 16 | count = DATA_PER_PAGE, 17 | email = "", 18 | }) { 19 | const [members, setMembers] = useState([]); 20 | const [lastDoc, setLastDoc] = useState(); 21 | const [total, setTotal] = useState(0); 22 | useEffect(() => { 23 | async function fetchData() { 24 | let colRef = collection(db, "users"); 25 | colRef = query(colRef, limit(count)); 26 | if (status) 27 | colRef = query(colRef, where("status", "==", status), limit(count)); 28 | if (email) 29 | colRef = query(colRef, where("email", "==", email), limit(count)); 30 | 31 | const documentSnapshots = await getDocs(colRef); 32 | const lastVisible = 33 | documentSnapshots.docs[documentSnapshots.docs.length - 1]; 34 | setLastDoc(lastVisible); 35 | onSnapshot(collection(db, "users"), (querySnapshot) => { 36 | setTotal(querySnapshot.size); 37 | }); 38 | onSnapshot(colRef, (querySnapshot) => { 39 | const results = []; 40 | querySnapshot.forEach((doc) => { 41 | results.push({ id: doc.id, ...doc.data() }); 42 | }); 43 | setMembers(results); 44 | }); 45 | } 46 | fetchData(); 47 | }, [status, count, email]); 48 | const handleLoadMore = async () => { 49 | let colRef = collection(db, "users"); 50 | colRef = query(colRef, startAfter(lastDoc || 0), limit(count)); 51 | 52 | if (status) { 53 | colRef = query( 54 | colRef, 55 | startAfter(lastDoc || 0), 56 | where("status", "==", status), 57 | limit(count) 58 | ); 59 | } 60 | if (email) 61 | colRef = query( 62 | colRef, 63 | startAfter(lastDoc || 0), 64 | where("email", "==", email), 65 | limit(count) 66 | ); 67 | const documentSnapshots = await getDocs(colRef); 68 | const lastVisible = 69 | documentSnapshots.docs[documentSnapshots.docs.length - 1]; 70 | setLastDoc(lastVisible); 71 | onSnapshot(colRef, (snapshot) => { 72 | let results = []; 73 | snapshot.forEach((doc) => { 74 | results.push({ 75 | id: doc.id, 76 | ...doc.data(), 77 | }); 78 | }); 79 | setMembers([...members, ...results]); 80 | }); 81 | }; 82 | return { 83 | members, 84 | handleLoadMore, 85 | isReachingEnd: total < members.length, 86 | total, 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /hooks/useInputChange.js: -------------------------------------------------------------------------------- 1 | export default function useInputChange(values, setValues) { 2 | const onChange = (e) => { 3 | const { name, value } = e.target; 4 | setValues({ ...values, [name]: value.replace(/ +/g, "") }); 5 | }; 6 | return { 7 | onChange, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function useLocalStorage(key, initialValue) { 4 | const [storedValue, setStoredValue] = useState(() => { 5 | if (typeof window === "undefined") { 6 | return initialValue; 7 | } 8 | try { 9 | const item = window.localStorage.getItem(key); 10 | return item ? JSON.parse(item) : initialValue; 11 | } catch (error) { 12 | console.log(error); 13 | return initialValue; 14 | } 15 | }); 16 | const setValue = (value) => { 17 | try { 18 | const valueToStore = 19 | value instanceof Function ? value(storedValue) : value; 20 | setStoredValue(valueToStore); 21 | if (typeof window !== "undefined") { 22 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 23 | } 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | }; 28 | return [storedValue, setValue]; 29 | } 30 | -------------------------------------------------------------------------------- /hooks/useToggle.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function useToggle() { 4 | const [show, setShow] = useState(false); 5 | const toggle = () => { 6 | setShow(!show); 7 | }; 8 | return { 9 | show, 10 | toggle, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./" 4 | }, 5 | 6 | "target": "ES6", 7 | "lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include"] 8 | } 9 | -------------------------------------------------------------------------------- /modules/card/CardAction.js: -------------------------------------------------------------------------------- 1 | import Card from "components/card/Card"; 2 | import FormGroup from "components/form/FormGroup"; 3 | import Label from "components/label/Label"; 4 | import React from "react"; 5 | 6 | const CardAction = ({ children, values = {} }) => { 7 | return ( 8 |
    9 | {children} 10 |
    11 | 12 | 13 | 21 | 22 |
    23 |
    24 | ); 25 | }; 26 | 27 | export default CardAction; 28 | -------------------------------------------------------------------------------- /modules/card/CardAddNew.js: -------------------------------------------------------------------------------- 1 | import Button from "components/button/Button"; 2 | import CodeEditorBlock from "components/CodeEditorBlock"; 3 | import { db } from "components/firebase/firebase-config"; 4 | import FormGroup from "components/form/FormGroup"; 5 | import Input from "components/input/Input"; 6 | import Label from "components/label/Label"; 7 | import { cardStatus, userRole, userStatus } from "constant/global-constant"; 8 | import { useAuth } from "contexts/auth-context"; 9 | import { 10 | addDoc, 11 | collection, 12 | getDocs, 13 | serverTimestamp, 14 | } from "firebase/firestore"; 15 | import useInputChange from "hooks/useInputChange"; 16 | import useToggle from "hooks/useToggle"; 17 | import React, { useEffect, useState } from "react"; 18 | import { toast } from "react-toastify"; 19 | import CardAction from "./CardAction"; 20 | import CardFilterDropdown from "./CardFilterDropdown"; 21 | 22 | const CardAddNew = () => { 23 | const { userInfo } = useAuth(); 24 | const [filterList, setFilterList] = useState([]); 25 | const [loading, setLoading] = useState(false); 26 | const [values, setValues] = useState({ 27 | title: "", 28 | filter: "", 29 | htmlCode: "", 30 | cssCode: "", 31 | author: "", 32 | }); 33 | const handleAddNewCard = (e) => { 34 | e.preventDefault(); 35 | if (userInfo?.status === userStatus.INACTIVE) { 36 | toast.warning("Your account is not active, please contact admin"); 37 | return; 38 | } 39 | const newValues = { ...values }; 40 | delete newValues.author; 41 | const isAllInputFilled = Object.values(newValues).every((item) => { 42 | return item !== ""; 43 | }); 44 | if (!isAllInputFilled) { 45 | toast.error("Please fill all inputs"); 46 | return; 47 | } 48 | setLoading(true); 49 | const colRef = collection(db, "cards"); 50 | try { 51 | addDoc(colRef, { 52 | ...values, 53 | reason: "", 54 | status: cardStatus.PENDING, 55 | createdAt: serverTimestamp(), 56 | userId: userInfo?.uid, 57 | userFullname: userInfo?.fullname, 58 | userEmailAddress: userInfo?.email, 59 | }); 60 | toast.success("Card added successfully and waiting for admin approval"); 61 | } catch (err) { 62 | toast.error(err.message); 63 | } finally { 64 | setValues({ 65 | title: "", 66 | filter: "", 67 | htmlCode: "", 68 | cssCode: "", 69 | author: "", 70 | }); 71 | setLoading(false); 72 | } 73 | }; 74 | useEffect(() => { 75 | const fetchData = async () => { 76 | const colRef = collection(db, "filters"); 77 | const snapshot = await getDocs(colRef); 78 | const data = snapshot.docs.map((doc) => doc.data()); 79 | setFilterList(data); 80 | }; 81 | fetchData(); 82 | }, []); 83 | const { onChange } = useInputChange(values, setValues); 84 | const { show: showFilter, toggle } = useToggle(); 85 | const handleSelectFilter = (filter) => { 86 | setValues({ 87 | ...values, 88 | filter, 89 | }); 90 | toggle(); 91 | }; 92 | return ( 93 | 94 |
    95 |
    96 | 97 | 98 | 106 | 107 | 108 | 109 | 115 | 116 |
    117 | 118 | 119 | 120 | 127 | 128 | 129 | 130 | 137 | 138 |
    139 | 140 | 141 | 148 | 149 |
    150 |
    151 | 154 |
    155 |
    156 |
    157 | ); 158 | }; 159 | 160 | export default CardAddNew; 161 | -------------------------------------------------------------------------------- /modules/card/CardFilterDropdown.js: -------------------------------------------------------------------------------- 1 | import Dropdown from "components/dropdown/Dropdown"; 2 | import { db } from "components/firebase/firebase-config"; 3 | import { filterStatus } from "constant/global-constant"; 4 | import { collection, getDocs, query, where } from "firebase/firestore"; 5 | import React, { useEffect, useState } from "react"; 6 | 7 | const CardFilterDropdown = ({ 8 | show = false, 9 | onClick = () => {}, 10 | onClickItem = () => {}, 11 | placeholder = null, 12 | }) => { 13 | const [filterList, setFilterList] = useState([]); 14 | useEffect(() => { 15 | const fetchData = async () => { 16 | let colRef = collection(db, "filters"); 17 | colRef = query(colRef, where("status", "==", filterStatus.APPROVED)); 18 | const snapshot = await getDocs(colRef); 19 | const data = snapshot.docs.map((doc) => doc.data()); 20 | setFilterList(data); 21 | }; 22 | fetchData(); 23 | }, []); 24 | return ( 25 | 30 | {filterList.map((item) => ( 31 |
    onClickItem(item.name)} 35 | > 36 | {item.name} 37 |
    38 | ))} 39 |
    40 | ); 41 | }; 42 | 43 | export default CardFilterDropdown; 44 | -------------------------------------------------------------------------------- /modules/card/CardManage.js: -------------------------------------------------------------------------------- 1 | import IconQuestion from "./../../components/icons/IconQuestion"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import Link from "next/link"; 5 | import LabelStatus from "components/label/LabelStatus"; 6 | import ButtonAction from "components/button/ButtonAction"; 7 | import Button from "components/button/Button"; 8 | import useFetchCards from "hooks/useFetchCards"; 9 | import { cardStatus, DATA_PER_PAGE, userRole } from "constant/global-constant"; 10 | import { IconEdit, IconTrash } from "components/icons"; 11 | import { collection, deleteDoc, doc } from "firebase/firestore"; 12 | import { db } from "components/firebase/firebase-config"; 13 | import { toast } from "react-toastify"; 14 | import Swal from "sweetalert2"; 15 | import ButtonNew from "components/button/ButtonNew"; 16 | import Input from "components/input/Input"; 17 | import Dropdown from "components/dropdown/Dropdown"; 18 | import useInputChange from "hooks/useInputChange"; 19 | import DropdownItem from "components/dropdown/DropdownItem"; 20 | import CardFilterDropdown from "./CardFilterDropdown"; 21 | import useToggle from "hooks/useToggle"; 22 | import { debounce } from "lodash"; 23 | import { useAuth } from "contexts/auth-context"; 24 | import Checkbox from "components/checkbox/Checkbox"; 25 | import { globalStore } from "store/global-store"; 26 | import shallow from "zustand/shallow"; 27 | 28 | const CardManage = (props) => { 29 | const [filter, setFilter] = React.useState(""); 30 | const [name, setName] = React.useState(""); 31 | const [status, setStatus] = React.useState(null); 32 | const [statusText, setStatusText] = React.useState(""); 33 | const { show, toggle } = useToggle(); 34 | const { show: showStatus, toggle: toggleStatus } = useToggle(); 35 | const handleClickFilter = (item) => { 36 | setFilter(item); 37 | toggle(); 38 | }; 39 | const handleClickStatus = (item) => { 40 | setStatus(item); 41 | setStatusText(item === cardStatus.APPROVED ? "Approved" : "Rejected"); 42 | toggleStatus(); 43 | }; 44 | const { cards, handleLoadMore, isReachingEnd, total } = useFetchCards({ 45 | status, 46 | name, 47 | filter, 48 | count: DATA_PER_PAGE, 49 | isManage: true, 50 | }); 51 | const resetSearch = () => { 52 | setName(""); 53 | setStatus(null); 54 | setFilter(""); 55 | setStatusText(""); 56 | }; 57 | const handleFilterByTitle = debounce((e) => { 58 | setName(e.target.value); 59 | }, 500); 60 | return ( 61 |
    62 | 63 |
    64 |
    65 | 71 |
    72 |
    73 | 78 | handleClickStatus(cardStatus.APPROVED)} 80 | > 81 | Approved 82 | 83 | handleClickStatus(cardStatus.PENDING)}> 84 | Pending 85 | 86 | handleClickStatus(cardStatus.REJECTED)} 88 | > 89 | Reject 90 | 91 | 92 |
    93 |
    94 | 100 |
    101 | 107 |
    108 |
    Found: {total}
    109 |
    110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {cards.length === 0 && ( 123 | 124 | 125 | 126 | )} 127 | {cards.length > 0 && 128 | cards.map((card) => ( 129 | 130 | ))} 131 | 132 |
    TitleFilterStatusCreatedAtAuthorActions
    No data
    133 |
    134 | {!isReachingEnd && ( 135 | 141 | )} 142 |
    143 | ); 144 | }; 145 | 146 | const CardRow = ({ card }) => { 147 | const { userInfo } = useAuth(); 148 | const renderStatus = (status) => { 149 | switch (status) { 150 | case cardStatus.APPROVED: 151 | return Approved; 152 | case cardStatus.PENDING: 153 | return Pending; 154 | 155 | default: 156 | return Rejected; 157 | } 158 | }; 159 | const handleDeleteCard = async (id) => { 160 | if (userInfo?.role !== userRole.ADMIN) { 161 | return; 162 | } 163 | try { 164 | const docRef = doc(db, "cards", id); 165 | Swal.fire({ 166 | title: "Are you sure?", 167 | icon: "warning", 168 | showCancelButton: true, 169 | confirmButtonText: "Yes, delete it!", 170 | }).then(async (result) => { 171 | if (result.isConfirmed) { 172 | await deleteDoc(docRef); 173 | toast.success("Delete card successfully"); 174 | } 175 | }); 176 | } catch (error) { 177 | toast.error("Delete card failed"); 178 | } 179 | }; 180 | const { setIsShowReasonModal, setReason } = globalStore( 181 | (state) => ({ 182 | setReason: state.setReason, 183 | isShowReasonModal: state.isShowReasonModal, 184 | setIsShowReasonModal: state.setIsShowReasonModal, 185 | }), 186 | shallow 187 | ); 188 | const handleShowReason = (reason) => { 189 | setIsShowReasonModal(true); 190 | setReason(reason); 191 | }; 192 | return ( 193 | 194 | {/* 195 | 196 | */} 197 | 198 | 199 | {card.title} 200 | 201 | 202 | {card.filter} 203 | {renderStatus(card.status)} 204 | 205 | {new Date(card.createdAt?.seconds * 1000).toLocaleDateString("vi-VI")} 206 | 207 | 208 |
    209 |

    {card.userFullname || "Admin"}

    210 |

    {card.userEmailAddress}

    211 |
    212 | 213 | 214 |
    215 | handleShowReason(card.reason || "")} 218 | > 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | {userInfo?.role === userRole.ADMIN && ( 230 | handleDeleteCard(card.id)} 233 | > 234 | 235 | 236 | )} 237 |
    238 | 239 | 240 | ); 241 | }; 242 | 243 | CardManage.propTypes = {}; 244 | 245 | export default CardManage; 246 | -------------------------------------------------------------------------------- /modules/card/CardUpdate.js: -------------------------------------------------------------------------------- 1 | import Button from "components/button/Button"; 2 | import CodeEditorBlock from "components/CodeEditorBlock"; 3 | import { db } from "components/firebase/firebase-config"; 4 | import FormGroup from "components/form/FormGroup"; 5 | import Input from "components/input/Input"; 6 | import Label from "components/label/Label"; 7 | import Toggle from "components/toggle/Toggle"; 8 | import { cardStatus, userRole, userStatus } from "constant/global-constant"; 9 | import { doc, getDoc, updateDoc } from "firebase/firestore"; 10 | import useInputChange from "hooks/useInputChange"; 11 | import useToggle from "hooks/useToggle"; 12 | import pretty from "pretty"; 13 | import React, { useEffect, useState } from "react"; 14 | import { toast } from "react-toastify"; 15 | import cssbeautify from "cssbeautify"; 16 | import CardFilterDropdown from "./CardFilterDropdown"; 17 | import CardAction from "./CardAction"; 18 | import { useAuth } from "contexts/auth-context"; 19 | 20 | const CardUpdate = ({ id }) => { 21 | const { userInfo } = useAuth(); 22 | const [values, setValues] = useState({ 23 | title: "", 24 | filter: "", 25 | htmlCode: "", 26 | cssCode: "", 27 | author: "", 28 | }); 29 | const [loading, setLoading] = useState(false); 30 | const { onChange } = useInputChange(values, setValues); 31 | useEffect(() => { 32 | async function fetchData() { 33 | const docRef = doc(db, "cards", id); 34 | const docSnap = await getDoc(docRef); 35 | if (docSnap.exists()) { 36 | setValues({ 37 | ...docSnap.data(), 38 | htmlCode: docSnap.data().htmlCode, 39 | cssCode: docSnap.data().cssCode, 40 | id: docSnap.id, 41 | }); 42 | } 43 | } 44 | fetchData(); 45 | }, [id]); 46 | const handleUpdateCard = async (e) => { 47 | e.preventDefault(); 48 | if (userInfo?.status === userStatus.INACTIVE) { 49 | toast.warning("Your account is not active, please contact admin"); 50 | return; 51 | } 52 | try { 53 | setLoading(true); 54 | const docRef = doc(db, "cards", id); 55 | await updateDoc(docRef, { 56 | ...values, 57 | }); 58 | toast.success("Card updated successfully"); 59 | } catch (err) { 60 | toast.error(err.message); 61 | } finally { 62 | setLoading(false); 63 | } 64 | }; 65 | const handleToggleStatus = () => { 66 | setValues({ 67 | ...values, 68 | status: 69 | values.status === cardStatus.APPROVED 70 | ? cardStatus.REJECTED 71 | : cardStatus.APPROVED, 72 | }); 73 | }; 74 | const { show: showFilter, toggle } = useToggle(); 75 | const handleSelectFilter = (filter) => { 76 | setValues({ 77 | ...values, 78 | filter, 79 | }); 80 | toggle(); 81 | }; 82 | return ( 83 | 84 |
    85 | {userInfo?.role === userRole.ADMIN && ( 86 | <> 87 |
    88 | 89 | 90 | 95 | 96 |
    97 | 98 | 99 | 105 | 106 | 107 | )} 108 |
    109 | 110 | 111 | 119 | 120 | 121 | 122 | 128 | 129 |
    130 | 131 | 132 | 133 | 140 | 141 | 142 | 143 | 150 | 151 |
    152 | 153 | 154 | 161 | 162 |
    163 | 164 |
    165 | 168 |
    169 |
    170 |
    171 | ); 172 | }; 173 | 174 | export default CardUpdate; 175 | -------------------------------------------------------------------------------- /modules/filter/FilterAddNew.js: -------------------------------------------------------------------------------- 1 | import Button from "components/button/Button"; 2 | import { db } from "components/firebase/firebase-config"; 3 | import FormGroup from "components/form/FormGroup"; 4 | import Input from "components/input/Input"; 5 | import Label from "components/label/Label"; 6 | import Toggle from "components/toggle/Toggle"; 7 | import { filterStatus, userRole } from "constant/global-constant"; 8 | import { useAuth } from "contexts/auth-context"; 9 | import { addDoc, collection, serverTimestamp } from "firebase/firestore"; 10 | import useInputChange from "hooks/useInputChange"; 11 | import React, { useState } from "react"; 12 | import { toast } from "react-toastify"; 13 | 14 | const FilterAddNew = () => { 15 | const { userInfo } = useAuth(); 16 | const [values, setValues] = useState({ 17 | name: "", 18 | status: filterStatus.REJECTED, 19 | }); 20 | const { onChange } = useInputChange(values, setValues); 21 | const handleAddNewFilter = (e) => { 22 | e.preventDefault(); 23 | if (userInfo?.role !== userRole.ADMIN) { 24 | toast.error("This feature only for admin!"); 25 | return; 26 | } 27 | const isAllInputFilled = Object.values(values).every((item) => item !== ""); 28 | if (!isAllInputFilled) { 29 | toast.error("Please fill all inputs"); 30 | return; 31 | } 32 | const colRef = collection(db, "filters"); 33 | try { 34 | addDoc(colRef, { 35 | ...values, 36 | name: values.name.toLowerCase(), 37 | createdAt: serverTimestamp(), 38 | }); 39 | toast.success("Filter added successfully"); 40 | } catch (err) { 41 | toast.error(err.message); 42 | } finally { 43 | setValues({ 44 | name: "", 45 | status: filterStatus.REJECTED, 46 | }); 47 | } 48 | }; 49 | const handleToggleStatus = () => { 50 | setValues({ 51 | ...values, 52 | status: !values.status, 53 | }); 54 | }; 55 | return ( 56 |
    57 |
    62 | 63 | 64 | 72 | 73 | 74 | 75 | 80 | 81 | 82 |
    83 | 84 |
    85 |
    86 |
    87 | ); 88 | }; 89 | 90 | export default FilterAddNew; 91 | -------------------------------------------------------------------------------- /modules/filter/FilterManage.js: -------------------------------------------------------------------------------- 1 | import ButtonAction from "components/button/ButtonAction"; 2 | import ButtonNew from "components/button/ButtonNew"; 3 | import { db } from "components/firebase/firebase-config"; 4 | import { IconEdit, IconTrash } from "components/icons"; 5 | import LabelStatus from "components/label/LabelStatus"; 6 | import { filterStatus, userRole } from "constant/global-constant"; 7 | import { useAuth } from "contexts/auth-context"; 8 | import { deleteDoc, doc } from "firebase/firestore"; 9 | import useFetchFilter from "hooks/useFetchFilter"; 10 | import Link from "next/link"; 11 | import React from "react"; 12 | import { toast } from "react-toastify"; 13 | import Swal from "sweetalert2"; 14 | 15 | const FilterManage = () => { 16 | const { filters } = useFetchFilter(); 17 | if (filters.length <= 0) return null; 18 | return ( 19 |
    20 | 21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {filters.map((filter) => ( 33 | 34 | ))} 35 | 36 |
    NameStatusCreatedAtActions
    37 |
    38 |
    39 | ); 40 | }; 41 | 42 | const FilterRow = ({ filter }) => { 43 | const { userInfo } = useAuth(); 44 | const renderStatus = (status) => { 45 | switch (status) { 46 | case filterStatus.APPROVED: 47 | return Approved; 48 | 49 | default: 50 | return Rejected; 51 | } 52 | }; 53 | const handleDeleteFilter = async (id) => { 54 | if (userInfo?.role !== userRole.ADMIN) { 55 | toast.error("This feature only for admin!"); 56 | return; 57 | } 58 | try { 59 | const docRef = doc(db, "filters", id); 60 | Swal.fire({ 61 | title: "Are you sure?", 62 | icon: "warning", 63 | showCancelButton: true, 64 | confirmButtonText: "Yes, delete it!", 65 | }).then(async (result) => { 66 | if (result.isConfirmed) { 67 | await deleteDoc(docRef); 68 | toast.success("Delete filter successfully"); 69 | } 70 | }); 71 | } catch (error) { 72 | toast.error("Delete filter failed"); 73 | } 74 | }; 75 | return ( 76 | 77 | {filter.name} 78 | {renderStatus(filter.status)} 79 | 80 | {new Date(filter.createdAt?.seconds * 1000).toLocaleDateString("vi-VI")} 81 | 82 | 83 |
    84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | handleDeleteFilter(filter.id)} 94 | > 95 | 96 | 97 |
    98 | 99 | 100 | ); 101 | }; 102 | 103 | export default FilterManage; 104 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | // const nextConfig = { 3 | // reactStrictMode: true, 4 | // swcMinify: true, 5 | // }; 6 | const removeImports = require("next-remove-imports")(); 7 | module.exports = removeImports({ 8 | experimental: { esmExternals: true }, 9 | }); 10 | 11 | // module.exports = nextConfig; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coding-ui", 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 | "@uiw/react-textarea-code-editor": "^2.0.3", 13 | "babel-plugin-styled-components": "^2.0.7", 14 | "cssbeautify": "^0.3.1", 15 | "firebase": "^9.9.1", 16 | "html-react-parser": "^4.2.1", 17 | "json-server": "^0.17.0", 18 | "lazysizes": "^5.3.2", 19 | "lodash": "^4.17.21", 20 | "next": "12.2.3", 21 | "next-remove-imports": "^1.0.6", 22 | "pretty": "^2.0.0", 23 | "prismjs": "^1.28.0", 24 | "prop-types": "^15.8.1", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "react-hook-form": "^7.34.0", 28 | "react-modal": "^3.15.1", 29 | "react-simple-code-editor": "^0.11.2", 30 | "react-syntax-highlighter": "^15.5.0", 31 | "react-toastify": "^9.0.7", 32 | "sass": "^1.54.0", 33 | "styled-components": "^5.3.5", 34 | "sweetalert2": "^11.4.26", 35 | "uuid": "^8.3.2", 36 | "zustand": "^4.0.0" 37 | }, 38 | "devDependencies": { 39 | "autoprefixer": "^10.4.7", 40 | "eslint": "8.20.0", 41 | "eslint-config-next": "12.2.3", 42 | "postcss": "^8.4.14", 43 | "tailwindcss": "^3.1.6" 44 | }, 45 | "browserslist": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /pages/404.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import React from "react"; 5 | 6 | const IconBack = ( 7 | 13 | 18 | 19 | ); 20 | const PageNotFound = () => { 21 | const router = useRouter(); 22 | const goBack = () => { 23 | router.push("/"); 24 | }; 25 | return ( 26 |
    27 | 28 | Oops! Page not found 29 | 30 | 37 | page-not-found 38 |
    39 | ); 40 | }; 41 | 42 | export default PageNotFound; 43 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.scss"; 2 | import { ToastContainer } from "react-toastify"; 3 | import "@uiw/react-textarea-code-editor/dist.css"; 4 | import "react-toastify/dist/ReactToastify.css"; 5 | import Modal from "react-modal"; 6 | import { AuthProvider } from "contexts/auth-context"; 7 | import "lazysizes"; 8 | import Head from "next/head"; 9 | import ModalReject from "components/modal/ModalReject"; 10 | import ModalViewCode from "components/modal/ModalViewCode"; 11 | 12 | Modal.setAppElement("#__next"); 13 | Modal.defaultStyles = { 14 | content: {}, 15 | }; 16 | 17 | function MyApp({ Component, pageProps }) { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default MyApp; 35 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
    9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Banner from "components/Banner"; 2 | import CardList from "components/card/CardList"; 3 | import FilterMenu from "components/filter/Filter"; 4 | import LayoutMain from "components/layout/LayoutMain"; 5 | import useFetchCards from "hooks/useFetchCards"; 6 | import Head from "next/head"; 7 | 8 | export default function Home() { 9 | return ( 10 | <> 11 | 12 | CodingUI - Get your free UI components with single click 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import Banner from "components/Banner"; 2 | import Button from "components/button/Button"; 3 | import { auth } from "components/firebase/firebase-config"; 4 | import FormGroup from "components/form/FormGroup"; 5 | import Input from "components/input/Input"; 6 | import Label from "components/label/Label"; 7 | import LayoutMain from "components/layout/LayoutMain"; 8 | import { useAuth } from "contexts/auth-context"; 9 | import { signInWithEmailAndPassword } from "firebase/auth"; 10 | import useInputChange from "hooks/useInputChange"; 11 | import { useRouter } from "next/router"; 12 | import React, { useEffect } from "react"; 13 | import { toast } from "react-toastify"; 14 | 15 | const LoginPage = () => { 16 | const { userInfo } = useAuth(); 17 | const router = useRouter(); 18 | useEffect(() => { 19 | if (userInfo?.email) router.push("/manage/cards"); 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, [userInfo]); 22 | const [values, setValues] = React.useState({ 23 | email: "", 24 | password: "", 25 | }); 26 | const { onChange } = useInputChange(values, setValues); 27 | const handleLogin = async (e) => { 28 | e.preventDefault(); 29 | const { email, password } = values; 30 | if (!email || !password) { 31 | toast.error("Please fill in all fields"); 32 | return; 33 | } 34 | try { 35 | await signInWithEmailAndPassword(auth, values.email, values.password); 36 | toast.success("Login successful"); 37 | router.push("/"); 38 | } catch (error) { 39 | console.log(error); 40 | toast.error(error.message); 41 | } 42 | }; 43 | return ( 44 | 45 |
    46 |
    47 | 48 | 49 | 56 | 57 | 58 | 59 | 66 | 67 | 73 |
    74 |
    75 |
    76 | ); 77 | }; 78 | 79 | export default LoginPage; 80 | -------------------------------------------------------------------------------- /pages/manage/cards.js: -------------------------------------------------------------------------------- 1 | import LayoutDashboard from "components/layout/LayoutDashboard"; 2 | import CardManage from "modules/card/CardManage"; 3 | import React from "react"; 4 | 5 | const CardManagePage = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default CardManagePage; 14 | -------------------------------------------------------------------------------- /pages/manage/filters.js: -------------------------------------------------------------------------------- 1 | import LayoutDashboard from "components/layout/LayoutDashboard"; 2 | import { userRole } from "constant/global-constant"; 3 | import { useAuth } from "contexts/auth-context"; 4 | import FilterManage from "modules/filter/FilterManage"; 5 | import React from "react"; 6 | 7 | const FilterPage = () => { 8 | const { userInfo } = useAuth(); 9 | return ( 10 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default FilterPage; 20 | -------------------------------------------------------------------------------- /pages/manage/new-card.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import LayoutDashboard from "components/layout/LayoutDashboard"; 4 | import CardAddNew from "modules/card/CardAddNew"; 5 | 6 | const AddNewCardPage = () => { 7 | return ( 8 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default AddNewCardPage; 19 | -------------------------------------------------------------------------------- /pages/manage/new-filter.js: -------------------------------------------------------------------------------- 1 | import LayoutDashboard from "components/layout/LayoutDashboard"; 2 | import { userRole } from "constant/global-constant"; 3 | import { useAuth } from "contexts/auth-context"; 4 | import FilterAddNew from "modules/filter/FilterAddNew"; 5 | import React from "react"; 6 | 7 | const AddFilter = () => { 8 | const { userInfo } = useAuth(); 9 | 10 | return ( 11 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default AddFilter; 22 | -------------------------------------------------------------------------------- /pages/manage/update-card.js: -------------------------------------------------------------------------------- 1 | import CardUpdate from "modules/card/CardUpdate"; 2 | import LayoutDashboard from "components/layout/LayoutDashboard"; 3 | import { useRouter } from "next/router"; 4 | import React from "react"; 5 | 6 | const UpdateCardPage = () => { 7 | const router = useRouter(); 8 | const { id } = router.query; 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default UpdateCardPage; 17 | -------------------------------------------------------------------------------- /pages/manage/update-filter.js: -------------------------------------------------------------------------------- 1 | import Button from "components/button/Button"; 2 | import { db } from "components/firebase/firebase-config"; 3 | import FormGroup from "components/form/FormGroup"; 4 | import Input from "components/input/Input"; 5 | import Label from "components/label/Label"; 6 | import LayoutDashboard from "components/layout/LayoutDashboard"; 7 | import Toggle from "components/toggle/Toggle"; 8 | import { filterStatus, userRole } from "constant/global-constant"; 9 | import { useAuth } from "contexts/auth-context"; 10 | import { doc, getDoc, updateDoc } from "firebase/firestore"; 11 | import useInputChange from "hooks/useInputChange"; 12 | import { useRouter } from "next/router"; 13 | import React, { useEffect, useState } from "react"; 14 | import { toast } from "react-toastify"; 15 | 16 | const UpdateFilterPage = () => { 17 | const router = useRouter(); 18 | const { id } = router.query; 19 | const { userInfo } = useAuth(); 20 | const [values, setValues] = useState({ 21 | name: "", 22 | status: filterStatus.REJECTED, 23 | }); 24 | useEffect(() => { 25 | async function fetchData() { 26 | const docRef = doc(db, "filters", id); 27 | const docSnap = await getDoc(docRef); 28 | if (docSnap.exists()) { 29 | setValues({ 30 | ...docSnap.data(), 31 | id: docSnap.id, 32 | }); 33 | } 34 | } 35 | fetchData(); 36 | }, [id]); 37 | const [loading, setLoading] = useState(false); 38 | const { onChange } = useInputChange(values, setValues); 39 | const handleUpdateFilter = async (e) => { 40 | if (userInfo?.role !== userRole.ADMIN) { 41 | toast.error("This feature only for admin!"); 42 | return; 43 | } 44 | e.preventDefault(); 45 | try { 46 | setLoading(true); 47 | const docRef = doc(db, "filters", id); 48 | await updateDoc(docRef, { 49 | ...values, 50 | }); 51 | toast.success("Update filter successfully"); 52 | } catch (err) { 53 | toast.error(err.message); 54 | } finally { 55 | setLoading(false); 56 | } 57 | }; 58 | const handleToggleStatus = () => { 59 | setValues({ 60 | ...values, 61 | status: !values.status, 62 | }); 63 | }; 64 | return ( 65 | 70 |
    71 |
    76 | 77 | 78 | 86 | 87 | 88 | 89 | 94 | 95 | 96 |
    97 | 105 |
    106 |
    107 |
    108 |
    109 | ); 110 | }; 111 | 112 | export default UpdateFilterPage; 113 | -------------------------------------------------------------------------------- /pages/manage/users.js: -------------------------------------------------------------------------------- 1 | import Button from "components/button/Button"; 2 | import ButtonAction from "components/button/ButtonAction"; 3 | import Checkbox from "components/checkbox/Checkbox"; 4 | import Dropdown from "components/dropdown/Dropdown"; 5 | import DropdownItem from "components/dropdown/DropdownItem"; 6 | import { db } from "components/firebase/firebase-config"; 7 | import { IconTrash } from "components/icons"; 8 | import Input from "components/input/Input"; 9 | import LabelStatus from "components/label/LabelStatus"; 10 | import LayoutDashboard from "components/layout/LayoutDashboard"; 11 | import { DATA_PER_PAGE, userRole, userStatus } from "constant/global-constant"; 12 | import { useAuth } from "contexts/auth-context"; 13 | import { deleteDoc, doc, updateDoc } from "firebase/firestore"; 14 | import useFetchMembers from "hooks/useFetchMembers"; 15 | import useToggle from "hooks/useToggle"; 16 | import { debounce } from "lodash"; 17 | import React from "react"; 18 | import { toast } from "react-toastify"; 19 | import Swal from "sweetalert2"; 20 | 21 | const ManageUsers = () => { 22 | const { userInfo } = useAuth(); 23 | const [email, setEmail] = React.useState(""); 24 | const [status, setStatus] = React.useState(null); 25 | const [statusText, setStatusText] = React.useState(""); 26 | const { show: showStatus, toggle: toggleStatus } = useToggle(); 27 | const { members, handleLoadMore, isReachingEnd, total } = useFetchMembers({ 28 | status, 29 | email: email, 30 | count: DATA_PER_PAGE, 31 | }); 32 | const resetSearch = () => { 33 | setEmail(""); 34 | setStatus(null); 35 | setStatusText(""); 36 | }; 37 | const handleFilterByEmail = debounce((e) => { 38 | setEmail(e.target.value); 39 | }, 500); 40 | const handleClickStatus = (item) => { 41 | setStatus(item); 42 | setStatusText(item === userStatus.ACTIVE ? "Active" : "Inactive"); 43 | toggleStatus(); 44 | }; 45 | return ( 46 | 50 |
    51 |
    52 | 58 |
    59 |
    60 | 65 | handleClickStatus(userStatus.ACTIVE)}> 66 | Active 67 | 68 | handleClickStatus(userStatus.INACTIVE)} 70 | > 71 | Inactive 72 | 73 | 74 |
    75 | 81 |
    82 |
    Found: {total}
    83 |
    84 | 85 | 86 | 87 | {/* */} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | {members.length > 0 && 97 | members.map((member) => ( 98 | 99 | ))} 100 | 101 |
     Full nameEmailStatusCreatedAtActions
    102 |
    103 | {!isReachingEnd && ( 104 | 110 | )} 111 |
    112 | ); 113 | }; 114 | 115 | function UserItem({ member }) { 116 | const { userInfo } = useAuth(); 117 | if (!member) return null; 118 | const handleDeleteMember = async (id) => { 119 | if (userInfo?.role !== userRole.ADMIN) { 120 | toast.error("This feature only for admin!"); 121 | return; 122 | } 123 | try { 124 | const docRef = doc(db, "users", id); 125 | Swal.fire({ 126 | title: "Are you sure?", 127 | icon: "warning", 128 | showCancelButton: true, 129 | confirmButtonText: "Yes, delete it!", 130 | }).then(async (result) => { 131 | if (result.isConfirmed) { 132 | await deleteDoc(docRef); 133 | toast.success("Delete user successfully"); 134 | } 135 | }); 136 | } catch (error) { 137 | toast.error("Delete user failed"); 138 | } 139 | }; 140 | const handleUpdateStatus = async (status) => { 141 | if (userInfo?.role !== userRole.ADMIN) { 142 | toast.error("This feature only for admin!"); 143 | return; 144 | } 145 | try { 146 | const docRef = doc(db, "users", member.id); 147 | await updateDoc(docRef, { 148 | status, 149 | }); 150 | toast.success("Update status successfully"); 151 | } catch (err) { 152 | console.log(err); 153 | toast.error("Update status failed"); 154 | } 155 | }; 156 | const renderStatus = (status) => { 157 | switch (status) { 158 | case userStatus.ACTIVE: 159 | return ( 160 | handleUpdateStatus(userStatus.INACTIVE)} 162 | className="bg-green-500" 163 | > 164 | Active 165 | 166 | ); 167 | default: 168 | return ( 169 | handleUpdateStatus(userStatus.ACTIVE)} 171 | className="bg-red-500" 172 | > 173 | Unactive 174 | 175 | ); 176 | } 177 | }; 178 | return ( 179 | 180 | {/* 181 | 182 | */} 183 | {member?.fullname || "Anonymous"} 184 | {member?.email} 185 | {renderStatus(member.status)} 186 | 187 | {new Date(member.createdAt?.seconds * 1000).toLocaleDateString("vi-VI")} 188 | 189 | 190 |
    191 | handleDeleteMember(member.id)} 194 | > 195 | 196 | 197 |
    198 | 199 | 200 | ); 201 | } 202 | 203 | export default ManageUsers; 204 | -------------------------------------------------------------------------------- /pages/signup.js: -------------------------------------------------------------------------------- 1 | import Button from "components/button/Button"; 2 | import { auth, db } from "components/firebase/firebase-config"; 3 | import FormGroup from "components/form/FormGroup"; 4 | import Input from "components/input/Input"; 5 | import Label from "components/label/Label"; 6 | import LayoutMain from "components/layout/LayoutMain"; 7 | import { userRole, userStatus } from "constant/global-constant"; 8 | import { useAuth } from "contexts/auth-context"; 9 | import { createUserWithEmailAndPassword } from "firebase/auth"; 10 | import { doc, serverTimestamp, setDoc } from "firebase/firestore"; 11 | import useInputChange from "hooks/useInputChange"; 12 | import { useRouter } from "next/router"; 13 | import React, { useEffect } from "react"; 14 | import { toast } from "react-toastify"; 15 | 16 | const CreateAccountPage = () => { 17 | const { userInfo } = useAuth(); 18 | const router = useRouter(); 19 | useEffect(() => { 20 | if (userInfo?.email) router.push("/manage/cards"); 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, [userInfo]); 23 | const [values, setValues] = React.useState({ 24 | email: "", 25 | password: "", 26 | name: "", 27 | }); 28 | const { onChange } = useInputChange(values, setValues); 29 | const handleSignUp = async (e) => { 30 | e.preventDefault(); 31 | const isAllInputFilled = Object.values(values).every((item) => item !== ""); 32 | if (!isAllInputFilled) { 33 | toast.error("Please fill all inputs"); 34 | return; 35 | } 36 | try { 37 | await createUserWithEmailAndPassword(auth, values.email, values.password); 38 | await setDoc(doc(db, "users", auth.currentUser.uid), { 39 | userId: auth.currentUser.uid, 40 | fullname: values.name, 41 | email: values.email, 42 | password: values.password, 43 | status: userStatus.ACTIVE, 44 | role: userRole.USER, 45 | createdAt: serverTimestamp(), 46 | }); 47 | toast.success("Create account successfully"); 48 | } catch (error) { 49 | console.log(error); 50 | toast.error(error.message); 51 | } 52 | }; 53 | return ( 54 | 55 |
    56 |
    57 | 58 | 59 | 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 86 | 87 | 93 |
    94 |
    95 |
    96 | ); 97 | }; 98 | 99 | export default CreateAccountPage; 100 | -------------------------------------------------------------------------------- /plan.md: -------------------------------------------------------------------------------- 1 | # Ý tưởng 2 | 3 | - Từ trang CollectUI.com chia sẻ các giao diện hình ảnh 4 | - Code ra 1 trang có tên là _CodingUI_ để chia sẻ các giao diện nhỏ và đơn giản kèm theo code, để mọi người vào lấy về dùng hoặc tham khảo học hỏi 5 | 6 | # Chức năng 7 | 8 | - Cho phép người dùng vào xem giao diện, xem code, sao chép code 9 | - Cho phép người dùng thả tim cho dự án 10 | - Cho phép người dùng lọc các giao diện theo chủ đề ví dụ như _Button_, _Loading_... 11 | - Lưu trữ các giao diện vào Database(Firebase, Firestore) 12 | - Có trang đăng nhập, đăng ký 13 | - Có trang Dashboard để quản lý các giao diện 14 | - Có chức năng thêm xóa sửa giao diện 15 | - Có chức năng phân quyền 16 | - Có chức năng lưu lại những cái đã thích 17 | 18 | # Công nghệ 19 | 20 | - Sử dụng ReactJS và NextJS 21 | - `styled-components` dùng để xử lý styles cho từng component riêng biệt 22 | - `html-react-parser` dùng để parse HTML ra giao diện 23 | - `pretty` dùng để format code HTML hiển thị cho đẹp 24 | - `cssbeautify` dùng để format code CSS 25 | - `react-syntax-highlighter` dùng để hiển thị cú pháp HTML CSS có màu cho đẹp và dễ nhìn 26 | - `react-toastify` dùng để hiển thị thông báo đơn giản 27 | - `tailwindcss` để code giao diện 28 | - `@uiw/react-textarea-code-editor` dùng để chỉnh sửa và hiển thị giao diện code HTML CSS JS đẹp và dễ nhìn 29 | - `firebase` dùng để lưu trữ dữ liệu 30 | - `react-hook-form` để làm việc với Form 31 | - `prop-types` dùng để check các props truyền vào component 32 | - `sweetalert2` dùng để hiển thị cảnh báo 33 | - `sass` code sass và tailwind 34 | - `react-modal` dùng để hiển thị modal/popup 35 | - `lodash` dùng để sài các hàm xử lý nho nhỏ 36 | - `zustand` để quản lý state 37 | 38 | # Thiết kế 39 | 40 | - Tham khảo các trang nước ngoài 41 | 42 | # Khó khăn 43 | 44 | - Làm 1 mình nên tự mò hết 45 | 46 | # Chi phí 47 | 48 | - Tốn thời gian, công sức, tên miền 49 | 50 | # Thành quả 51 | 52 | - Có 1 website xịn xò bỏ vào CV cũng như được nhiều người sử dụng và biết tới brand _Evondev_ 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evondev/coding-ui/79f805ef14ee15709af45753ad966f5f09cef666/public/404.png -------------------------------------------------------------------------------- /public/SFMonoLigaturized-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evondev/coding-ui/79f805ef14ee15709af45753ad966f5f09cef666/public/SFMonoLigaturized-Regular.ttf -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evondev/coding-ui/79f805ef14ee15709af45753ad966f5f09cef666/public/favicon.ico -------------------------------------------------------------------------------- /public/firacodevf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evondev/coding-ui/79f805ef14ee15709af45753ad966f5f09cef666/public/firacodevf.woff2 -------------------------------------------------------------------------------- /public/intervar.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evondev/coding-ui/79f805ef14ee15709af45753ad966f5f09cef666/public/intervar.woff2 -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evondev/coding-ui/79f805ef14ee15709af45753ad966f5f09cef666/public/logo.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /store/global-store.js: -------------------------------------------------------------------------------- 1 | import create from "zustand"; 2 | 3 | export const globalStore = create((set) => ({ 4 | // state 5 | isShowCode: false, 6 | htmlCodeView: "", 7 | cssCodeView: "", 8 | isShowReasonModal: false, 9 | reason: "", 10 | // actions 11 | setIsShowReasonModal: () => 12 | set((state) => ({ isShowReasonModal: !state.isShowReasonModal })), 13 | setIsShowCode: () => set((state) => ({ isShowCode: !state.isShowCode })), 14 | setHtmlCodeView: (htmlCodeView) => set({ htmlCodeView }), 15 | setCssCodeView: (cssCodeView) => set({ cssCodeView }), 16 | setReason: (reason) => set({ reason }), 17 | })); 18 | -------------------------------------------------------------------------------- /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 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /styles/_editor.scss: -------------------------------------------------------------------------------- 1 | // Editor 2 | 3 | body .w-tc-editor { 4 | --btn-gradient: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); 5 | --color-prettylights-syntax-entity: #ff6bcb; 6 | --color-prettylights-syntax-entity-tag: #ff6bcb; 7 | --color-prettylights-syntax-constant: #20e3b2; 8 | --color-prettylights-syntax-string: #20e3b2; 9 | --color-prettylights-syntax-sublimelinter-gutter-mark: #64748b; 10 | --color-prettylights-syntax-unit: #9a86fd; 11 | --color-prettylights-syntax-number: #ff6bcb; 12 | --color-prettylights-syntax-variable: #82aaff; 13 | --color-prettylights-syntax-attr: #20e3b2; 14 | --color-prettylights-syntax-attr-value: #eac394; 15 | --color-prettylights-syntax-property: #2cccff; 16 | @apply font-normal w-full p-4 text-sm text-white transition-all outline-none bg-slate-900 focus:border-blue-500 resize-none overflow-y-auto leading-loose font-code; 17 | textarea { 18 | @apply placeholder:font-primary placeholder:font-medium; 19 | } 20 | 21 | .code-highlight { 22 | @apply hidden; 23 | } 24 | code[class^="language-"].code-highlight { 25 | @apply block; 26 | } 27 | pre[class^="language-"] { 28 | .token.attr-name { 29 | color: var(--color-prettylights-syntax-attr); 30 | } 31 | .token.punctuation { 32 | color: #64748b; 33 | } 34 | .token.attr-value { 35 | color: var(--color-prettylights-syntax-attr-value); 36 | } 37 | .token.rule, 38 | .token.punctuation.attr-equals { 39 | color: var(--color-prettylights-syntax-entity); 40 | } 41 | .token.property { 42 | color: var(--color-prettylights-syntax-property); 43 | } 44 | .token.atrule { 45 | color: #bd93f9; 46 | } 47 | } 48 | pre[class^="language-css"] { 49 | color: #9a86fd; 50 | .token.unit { 51 | color: var(--color-prettylights-syntax-unit); 52 | } 53 | .token.number { 54 | color: var(--color-prettylights-syntax-number); 55 | } 56 | .token.variable { 57 | color: var(--color-prettylights-syntax-variable); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;700&display=swap"); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @layer base { 7 | @font-face { 8 | font-family: "SF mono"; 9 | src: url("/SFMonoLigaturized-Regular.ttf"); 10 | } 11 | @font-face { 12 | font-family: "Inter var"; 13 | src: url("/intervar.woff2"); 14 | } 15 | :root { 16 | --blue: #2cccff; 17 | --primary: var(--blue); 18 | --pink: #f62682; 19 | } 20 | html { 21 | scroll-behavior: smooth; 22 | } 23 | body { 24 | @apply bg-slate-900 font-primary text-white pb-10 font-light mx-auto overflow-x-hidden; 25 | max-width: 1920px; 26 | } 27 | button:disabled { 28 | @apply cursor-not-allowed opacity-50; 29 | } 30 | .scrollbar-style::-webkit-scrollbar, 31 | .w-tc-editor::-webkit-scrollbar, 32 | body::-webkit-scrollbar { 33 | width: 5px; 34 | } 35 | .scrollbar-style::-webkit-scrollbar-track, 36 | .w-tc-editor::-webkit-scrollbar-track, 37 | body::-webkit-scrollbar-track { 38 | border-radius: 100rem; 39 | } 40 | .scrollbar-style::-webkit-scrollbar-thumb, 41 | .w-tc-editor::-webkit-scrollbar-thumb, 42 | body::-webkit-scrollbar-thumb { 43 | border-radius: 100rem; 44 | // background-image: linear-gradient(245deg, #fcb564, #ff65f2); 45 | @apply bg-slate-600; 46 | } 47 | } 48 | @layer utilities { 49 | .bg-gradient-primary { 50 | background-image: linear-gradient(to right top, #6a5af9, #d66efd); 51 | } 52 | .bg-gradient-secondary { 53 | background-image: linear-gradient(to right top, #fc6c8f, #ffb86c); 54 | } 55 | .hidden-scroll { 56 | -ms-overflow-style: none; 57 | scrollbar-width: none; 58 | } 59 | .hidden-scroll::-webkit-scrollbar { 60 | display: none; 61 | } 62 | } 63 | .table { 64 | @apply w-full; 65 | th, 66 | td { 67 | @apply p-3 text-left whitespace-nowrap; 68 | } 69 | th { 70 | @apply text-slate-400; 71 | } 72 | } 73 | .l-container { 74 | @apply px-5 mx-auto max-w-7xl; 75 | } 76 | .button-effect { 77 | position: relative; 78 | isolation: isolate; 79 | overflow: hidden; 80 | } 81 | .button-effect:before { 82 | content: ""; 83 | position: absolute; 84 | left: 0; 85 | right: auto; 86 | top: 0; 87 | height: 100%; 88 | width: 0; 89 | background-color: white; 90 | opacity: 0.1; 91 | z-index: -1; 92 | transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1); 93 | } 94 | .button-effect:hover:before { 95 | left: auto; 96 | right: 0; 97 | width: 100%; 98 | } 99 | .pointer-effect { 100 | animation: pointer 1s infinite cubic-bezier(0.075, 0.82, 0.165, 1); 101 | } 102 | .ReactModal__Body--open { 103 | overflow: hidden; 104 | } 105 | .wrapper-code { 106 | @apply overflow-y-auto border rounded-lg border-slate-700; 107 | max-height: 300px; 108 | } 109 | @keyframes pointer { 110 | 100% { 111 | transform: scale(0.75); 112 | } 113 | } 114 | .heart-animate.is-active { 115 | animation: zoom 0.25s 1 linear; 116 | } 117 | @keyframes zoom { 118 | 0% { 119 | transform: scale(0); 120 | } 121 | 50% { 122 | transform: scale(1.2); 123 | } 124 | 100% { 125 | transform: scale(1); 126 | } 127 | } 128 | @import "editor"; 129 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: "media", 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | "./modules/**/*.{js,ts,jsx,tsx}", 8 | ], 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | primary: ["DM Sans", "sans-serif"], 13 | secondary: ["Intervar", "sans-serif"], 14 | code: ["SF mono", "sans-serif"], 15 | }, 16 | colors: { 17 | grayf5: "#F5F7FA", 18 | primary: "#fc6c8f", 19 | secondary: "#fcb564", 20 | third: "#6a5af9", 21 | fourth: "#d66efd", 22 | }, 23 | }, 24 | }, 25 | plugins: [], 26 | }; 27 | -------------------------------------------------------------------------------- /utils/classNames.js: -------------------------------------------------------------------------------- 1 | export default function classNames(...args) { 2 | return args 3 | .reduce((acc, val) => { 4 | if (typeof val === "string") { 5 | return acc.concat(val.split(" ")); 6 | } 7 | return acc.concat(Object.values(val)); 8 | }, []) 9 | .join(" "); 10 | } 11 | -------------------------------------------------------------------------------- /utils/copyToClipboard.js: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | 3 | export default function copyToClipBoard(text = "") { 4 | navigator.clipboard?.writeText && navigator.clipboard.writeText(text); 5 | toast.success("Copy to clipboard successfully"); 6 | } 7 | --------------------------------------------------------------------------------