├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── HOC └── withAuth.tsx ├── LICENSE.md ├── README.md ├── __tests__ ├── auth.test.tsx ├── components │ ├── snippng_code_area.test.tsx │ ├── snippng_control_header.test.tsx │ └── snippng_window_controls.test.tsx ├── index.test.tsx ├── lib │ └── color_picker.test.tsx └── utils.test.tsx ├── components ├── ErrorText.tsx ├── Loader.tsx ├── Logo.tsx ├── NoSSRWrapper.tsx ├── SigninButton.tsx ├── ThemeToggle.tsx ├── Toast.tsx ├── editor │ ├── SnippngCodeArea.tsx │ ├── SnippngConfigImportExporter.tsx │ ├── SnippngControlHeader.tsx │ ├── SnippngThemeBuilder.tsx │ └── SnippngWindowControls.tsx ├── explore │ └── PublishedThemeListing.tsx ├── form │ ├── Button.tsx │ ├── Checkbox.tsx │ ├── Input.tsx │ ├── Range.tsx │ └── Select.tsx ├── icons │ ├── GithubIcon.tsx │ └── GoogleIcon.tsx ├── index.tsx └── profile │ ├── SnippngListItem.tsx │ └── SnippngThemeItem.tsx ├── config └── firebase.ts ├── context ├── AuthContext.tsx ├── SnippngEditorContext.tsx └── ToastContext.tsx ├── docker-compose.yml ├── jest.config.js ├── jest.resolver.js ├── jest.setup.js ├── layout ├── Footer.tsx ├── Header.tsx └── Layout.tsx ├── lib ├── color-picker │ ├── components │ │ ├── ColorPicker.tsx │ │ └── variants │ │ │ ├── GradientSelect.tsx │ │ │ └── index.tsx │ ├── index.ts │ ├── types │ │ └── index.ts │ └── utils │ │ └── index.ts ├── constants.ts ├── image-picker │ ├── ImagePicker.tsx │ ├── index.tsx │ └── utils │ │ └── index.ts └── width-handler │ ├── WidthHandler.tsx │ └── index.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── _offline.tsx ├── explore │ └── themes │ │ └── index.tsx ├── index.tsx ├── profile.tsx ├── snippet │ └── [uid].tsx └── theme │ └── create.tsx ├── postcss.config.js ├── public ├── logo-192x192.png ├── logo-256x256.png ├── logo-512x512.png ├── logo-dark.svg ├── logo.svg └── manifest.json ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types ├── editor-ti.ts ├── editor.ts ├── index.ts └── toast.ts ├── utils └── index.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@next/next/no-img-element": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /.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 | .env 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .vscode 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # PWA files 42 | 43 | **/public/sw.js 44 | **/public/workbox-*.js 45 | **/public/worker-*.js 46 | **/public/sw.js.map 47 | **/public/workbox-*.js.map 48 | **/public/worker-*.js.map 49 | **/public/fallback-*.js.map 50 | **/public/fallback-*.js -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # deps stage 2 | FROM node:16-alpine3.17 as deps 3 | 4 | WORKDIR /app 5 | 6 | COPY package.json yarn.lock ./ 7 | 8 | RUN yarn --frozen-lockfile 9 | 10 | # build stage 11 | FROM node:16-alpine3.17 as BUILD_IMAGE 12 | 13 | WORKDIR /app 14 | 15 | COPY --from=deps /app/node_modules ./node_modules 16 | 17 | COPY . . 18 | 19 | ENV NEXT_TELEMETRY_DISABLED 1 20 | 21 | RUN yarn build 22 | 23 | 24 | # final stage 25 | FROM node:16-alpine3.17 as runner 26 | 27 | WORKDIR /app 28 | 29 | ENV NEXT_TELEMETRY_DISABLED 1 30 | 31 | RUN addgroup --system --gid 1001 nodejs 32 | RUN adduser --system --uid 1001 nextjs 33 | 34 | COPY --from=BUILD_IMAGE /app/public ./public 35 | 36 | COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/package.json /app/yarn.lock ./ 37 | COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/next.config.js ./ 38 | 39 | COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/node_modules ./node_modules 40 | 41 | COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/.next/standalone ./ 42 | COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/.next/static ./.next/static 43 | 44 | USER nextjs 45 | 46 | EXPOSE 3000 47 | 48 | ENV PORT 3000 49 | 50 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /HOC/withAuth.tsx: -------------------------------------------------------------------------------- 1 | import { SigninButton } from "@/components"; 2 | import { useAuth } from "@/context/AuthContext"; 3 | import Layout from "@/layout/Layout"; 4 | import { ComponentType } from "react"; 5 | 6 | /** 7 | * @function withAuth (HOC) 8 | * @description Handles the authentication logic and returns the wrapped `Component` if the user is `authenticated` else renders signin button 9 | */ 10 | const withAuth = (Component: ComponentType) => { 11 | const InnerComponent = (props: T) => { 12 | const { user } = useAuth(); 13 | 14 | if (!user) 15 | return ( 16 | 17 |
21 | 22 |
23 |
24 | ); 25 | 26 | return ; 27 | }; 28 | return InnerComponent; 29 | }; 30 | 31 | export default withAuth; 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shubham Waje 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 21 | 22 | [![Contributors][contributors-shield]][contributors-url] 23 | [![Forks][forks-shield]][forks-url] 24 | [![Stargazers][stars-shield]][stars-url] 25 | [![Issues][issues-shield]][issues-url] 26 | [![MIT License][license-shield]][license-url] 27 | [![LinkedIn][linkedin-shield]][linkedin-url] 28 | 29 | 30 |
31 |
32 | 33 | Logo 34 | 35 |

36 | Create and share beautiful images of your source code. 37 |
38 |
39 | Generate snippets 40 | · 41 | Report Bug 42 | · 43 | Request Feature 44 |

45 |
46 | 47 | 48 |
49 | Table of Contents 50 |
    51 |
  1. 52 | About The Project 53 | 56 |
  2. 57 |
  3. 58 | Getting Started 59 | 63 |
  4. 64 |
  5. Roadmap
  6. 65 |
  7. Contributing
  8. 66 |
  9. License
  10. 67 |
  11. Contact
  12. 68 | 69 |
70 |
71 | 72 | 73 | 74 | ## About The Project 75 | 76 | 77 | Logo 78 | 79 | 80 | Create and share beautiful images of your source code. Start typing or paste a code snippet into the text area to get started. 81 | 82 |

(back to top)

83 | 84 | ## Built With 85 | 86 | - [![Next][next.js]][next-url] 87 | - [![NodeJs][node.js]][node-url] 88 | - [![Typescript][typescript]][typescript-url] 89 | - [![React][react.js]][react-url] 90 | - [![tailwindcss][tailwindcss]][tailwindcss-url] 91 | - [![Firebase][firebase]][firebase-url] 92 | - [![Jest][jest]][jest-url] 93 | 94 |

(back to top)

95 | 96 | 97 | 98 | ## Getting Started 99 | 100 | To get a local copy up and running follow these steps. 101 | 102 | ### Prerequisites 103 | 104 | You need `NodeJs` and `yarn` installed on your machine. 105 | 106 | - yarn 107 | ```sh 108 | npm install --global yarn 109 | ``` 110 | 111 | ### Firebase prerequisites (optional) 112 | 113 | Firebase is used in this project for authentications and to store snippets. In order to contribute in the part requiring Firebase, create a file called `.env` inside the root folder and add the following credentials in it once you create a Firebase app. 114 | 115 | ```bash 116 | NEXT_PUBLIC_FIREBASE_API_KEY= 117 | 118 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= 119 | 120 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 121 | 122 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= 123 | 124 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= 125 | 126 | NEXT_PUBLIC_FIREBASE_APP_ID= 127 | 128 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= 129 | 130 | NEXT_PUBLIC_PEXELS_API_KEY= 131 | 132 | # don't change the following env var 133 | NEXT_PUBLIC_PEXELS_QUERY_URL=https://api.pexels.com/v1 134 | 135 | ``` 136 | 137 | It does not matter what credentials you add to your `.env` file, as the app won't crash while developing since the error is taken care of for the Firebase services that are unavailable. 138 | 139 | ### Installation 140 | 141 | 1. Clone the repo 142 | ```sh 143 | git clone https://github.com/wajeshubham/snippng.git 144 | ``` 145 | 2. Install NPM packages 146 | ```sh 147 | yarn 148 | ``` 149 | 150 | ### Run locally 151 | 152 | 1. Run the development server 153 | ```sh 154 | yarn dev 155 | ``` 156 | 2. Run the test to test your changes 157 | ```sh 158 | yarn test 159 | ``` 160 | 161 | OR 162 | 163 | 1. Run using `docker`. make sur you have [Docker](https://docs.docker.com/get-docker/) installed on your machine. 164 | ```sh 165 | docker-compose up --build 166 | ``` 167 | 168 |

(back to top)

169 | 170 | ## Roadmap 171 | 172 | - [ ] Add theme presets to choose from 173 | - [ ] Option to create/save single default editor config for user 174 | - [ ] Publish your themes 175 | - [x] Inject images in the background from url and pexels 176 | - [x] Import and export snippng config to quickly build the editor (download the JSON file with editor config) 177 | - [x] Custom theme configuration 178 | - [x] Build more themes to choose from 179 | 180 |

(back to top)

181 | 182 | 183 | 184 | ## Contributing 185 | 186 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 187 | 188 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 189 | Don't forget to give the project a star! Thanks again! 190 | 191 | 1. Fork the Project 192 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 193 | 3. Do some magic 🪄 inside the code. Write/Run tests before committing 194 | 4. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 195 | 5. Push to the Branch (`git push origin feature/AmazingFeature`) 196 | 6. Open a Pull Request 197 | 198 |

(back to top)

199 | 200 | 201 | 202 | ## License 203 | 204 | Distributed under the MIT License. See `LICENSE.md` for more information. 205 | 206 |

(back to top)

207 | 208 | 209 | 210 | ## Contact 211 | 212 | Shubham Waje - [@linkedin/shubham-waje](https://linkedin.com/in/shubham-waje) 213 | 214 | Project Link: [https://snippng.wajeshubham.in](https://snippng.wajeshubham.in) 215 | 216 |

(back to top)

217 | 218 | 219 | 220 | [contributors-shield]: https://img.shields.io/github/contributors/wajeshubham/snippng?style=for-the-badge 221 | [contributors-url]: https://github.com/wajeshubham/snippng/graphs/contributors 222 | [forks-shield]: https://img.shields.io/github/forks/wajeshubham/snippng?style=for-the-badge 223 | [forks-url]: https://github.com/wajeshubham/snippng/network/members 224 | [stars-shield]: https://img.shields.io/github/stars/wajeshubham/snippng?style=for-the-badge 225 | [stars-url]: https://github.com/wajeshubham/snippng/stargazers 226 | [issues-shield]: https://img.shields.io/github/issues/wajeshubham/snippng?style=for-the-badge 227 | [issues-url]: https://github.com/wajeshubham/snippng/issues 228 | [license-shield]: https://img.shields.io/github/license/wajeshubham/snippng?style=for-the-badge 229 | [license-url]: https://github.com/wajeshubham/snippng/blob/master/LICENSE.md 230 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 231 | [linkedin-url]: https://linkedin.com/in/shubham-waje 232 | [product-screenshot]: images/screenshot.png 233 | [next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white 234 | [next-url]: https://nextjs.org/ 235 | [react.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB 236 | [react-url]: https://reactjs.org/ 237 | [tailwindcss]: https://img.shields.io/badge/Tailwind-000000?style=for-the-badge&logo=tailwindcss&logoColor=white 238 | [tailwindcss-url]: https://tailwindcss.com 239 | [typescript]: https://img.shields.io/badge/Typescript-000000?style=for-the-badge&logo=typescript&logoColor=white 240 | [typescript-url]: https://typescriptlang.org 241 | [firebase]: https://img.shields.io/badge/firebase-000000?style=for-the-badge&logo=firebase&logoColor=ffa611 242 | [firebase-url]: https://firebase.google.com/ 243 | [node.js]: https://img.shields.io/badge/node.js-000000?style=for-the-badge&logo=node.js&logoColor=68a063 244 | [node-url]: https://nodejs.org/ 245 | [jest]: https://img.shields.io/badge/jest-000000?style=for-the-badge&logo=jest&logoColor=c21325 246 | [jest-url]: https://jestjs.io/ 247 | -------------------------------------------------------------------------------- /__tests__/auth.test.tsx: -------------------------------------------------------------------------------- 1 | import { AuthContext } from "@/context/AuthContext"; 2 | import Home from "@/pages"; 3 | import { render, screen, waitFor } from "@testing-library/react"; 4 | import { act } from "react-dom/test-utils"; 5 | 6 | beforeEach(() => { 7 | // IntersectionObserver isn't available in test environment 8 | const mockIntersectionObserver = jest.fn(); 9 | mockIntersectionObserver.mockReturnValue({ 10 | observe: () => null, 11 | unobserve: () => null, 12 | disconnect: () => null, 13 | }); 14 | window.IntersectionObserver = mockIntersectionObserver; 15 | }); 16 | 17 | beforeAll(() => { 18 | document.createRange = () => { 19 | const range = new Range(); 20 | 21 | range.getBoundingClientRect = jest.fn(); 22 | 23 | range.getClientRects = () => { 24 | return { 25 | item: () => null, 26 | length: 0, 27 | [Symbol.iterator]: jest.fn(), 28 | }; 29 | }; 30 | 31 | return range; 32 | }; 33 | }); 34 | 35 | const mockedUser = { 36 | displayName: "John Doe", 37 | email: "john@doe.com", 38 | uid: "uid-test", 39 | }; 40 | 41 | jest.mock("next/router", () => require("next-router-mock")); 42 | 43 | describe("Auth", () => { 44 | it("renders signin button for logged out user", async () => { 45 | await act(async () => { 46 | render(); 47 | }); 48 | screen.getByTestId("signin-btn"); 49 | }); 50 | 51 | it("renders username and logout button for logged in user", async () => { 52 | await act(async () => { 53 | render( 54 | // @ts-ignore 55 | 59 | 60 | 61 | ); 62 | }); 63 | await waitFor(() => { 64 | screen.getByText("John Doe"); 65 | screen.getByTestId("logout-btn"); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /__tests__/components/snippng_code_area.test.tsx: -------------------------------------------------------------------------------- 1 | import { SnippngCodeArea, SnippngControlHeader } from "@/components"; 2 | import { SnippngEditorContext } from "@/context/SnippngEditorContext"; 3 | import { defaultEditorConfig } from "@/lib/constants"; 4 | import { getEditorWrapperBg } from "@/utils"; 5 | import { render, screen, waitFor } from "@testing-library/react"; 6 | import { act } from "react-dom/test-utils"; 7 | 8 | jest.mock("next/router", () => require("next-router-mock")); 9 | 10 | beforeEach(() => { 11 | // IntersectionObserver isn't available in test environment 12 | const mockIntersectionObserver = jest.fn(); 13 | mockIntersectionObserver.mockReturnValue({ 14 | observe: () => null, 15 | unobserve: () => null, 16 | disconnect: () => null, 17 | }); 18 | window.IntersectionObserver = mockIntersectionObserver; 19 | }); 20 | 21 | beforeAll(() => { 22 | document.createRange = () => { 23 | const range = new Range(); 24 | 25 | range.getBoundingClientRect = jest.fn(); 26 | 27 | range.getClientRects = () => { 28 | return { 29 | item: () => null, 30 | length: 0, 31 | [Symbol.iterator]: jest.fn(), 32 | }; 33 | }; 34 | 35 | return range; 36 | }; 37 | }); 38 | 39 | describe("SnippngCodeArea", () => { 40 | it("renders the editor", async () => { 41 | await act(async () => { 42 | render(); 43 | }); 44 | await waitFor(() => { 45 | screen.getByTestId("snippng-code-area"); 46 | }); 47 | }); 48 | 49 | describe("showLineNumbers flag", () => { 50 | it("renders WITH line count when showLineNumbers flag is true", async () => { 51 | await act(async () => { 52 | render(); 53 | }); 54 | const lineGutters = document.querySelector(".cm-gutters"); 55 | await waitFor(() => { 56 | expect(lineGutters).toBeInTheDocument(); 57 | }); 58 | }); 59 | 60 | it("renders WITHOUT line count when showLineNumbers flag is false", async () => { 61 | await act(async () => { 62 | render( 63 | {}, 68 | // override showLineNumbers with false 69 | }} 70 | > 71 | 72 | 73 | ); 74 | }); 75 | const lineGutters = document.querySelector(".cm-gutters"); 76 | await waitFor(() => { 77 | expect(lineGutters).not.toBeInTheDocument(); 78 | }); 79 | }); 80 | }); 81 | 82 | describe("hasDropShadow flag", () => { 83 | it("renders WITH box-shadow when hasDropShadow flag is true", async () => { 84 | await act(async () => { 85 | render(); 86 | }); 87 | const editorWithShadow = document.querySelector( 88 | ".has-drop-shadow-testclass" 89 | ); 90 | await waitFor(() => { 91 | expect(editorWithShadow).toBeInTheDocument(); 92 | }); 93 | }); 94 | 95 | it("renders WITHOUT box-shadow when hasDropShadow flag is false", async () => { 96 | await act(async () => { 97 | render( 98 | {}, 103 | // override hasDropShadow with false 104 | }} 105 | > 106 | 107 | 108 | ); 109 | }); 110 | const editorWIthShadow = document.querySelector( 111 | ".has-drop-shadow-testclass" 112 | ); 113 | await waitFor(() => { 114 | expect(editorWIthShadow).not.toBeInTheDocument(); 115 | }); 116 | }); 117 | }); 118 | 119 | describe("rounded flag", () => { 120 | it("renders WITH rounded corners when rounded flag is true", async () => { 121 | await act(async () => { 122 | render(); 123 | }); 124 | const editorWithShadow = document.querySelector(".rounded-testclass"); 125 | await waitFor(() => { 126 | expect(editorWithShadow).toBeInTheDocument(); 127 | }); 128 | }); 129 | 130 | it("renders WITHOUT rounded corners when rounded flag is false", async () => { 131 | await act(async () => { 132 | render( 133 | {}, 138 | // override rounded with false 139 | }} 140 | > 141 | 142 | 143 | ); 144 | }); 145 | const editorWIthShadow = document.querySelector(".rounded-testclass"); 146 | await waitFor(() => { 147 | expect(editorWIthShadow).not.toBeInTheDocument(); 148 | }); 149 | }); 150 | }); 151 | 152 | describe("showFilename flag", () => { 153 | it("renders the editor WITH file name when showFilename flag is true", async () => { 154 | await act(async () => { 155 | render(); 156 | }); 157 | const fileNameInput = document.getElementById( 158 | "file-name-input" 159 | ) as HTMLInputElement; 160 | const value = fileNameInput.value; 161 | await waitFor(() => { 162 | expect(value).toBe("@utils/debounce.ts"); 163 | }); 164 | }); 165 | 166 | it("renders the editor WITHOUT file name when showFilename flag is false", async () => { 167 | await act(async () => { 168 | render( 169 | {}, 174 | // override hasDropShadow with false 175 | }} 176 | > 177 | 178 | 179 | ); 180 | }); 181 | const fileNameInput = document.getElementById( 182 | "file-name-input" 183 | ) as HTMLInputElement; 184 | await waitFor(() => { 185 | expect(fileNameInput).not.toBeInTheDocument(); 186 | }); 187 | }); 188 | }); 189 | 190 | describe("wrapperBg property", () => { 191 | it("renders wrapper with background-color equals to the color selected with color picker input", async () => { 192 | await act(async () => { 193 | render( 194 | {}, 203 | // override hasDropShadow with false 204 | }} 205 | > 206 | 207 | 208 | ); 209 | 210 | // @ts-ignore 211 | render(); 212 | }); 213 | const colorPicker = document.getElementById( 214 | "color-picker" 215 | ) as HTMLInputElement; 216 | const wrapper = document.getElementById("code-wrapper") as HTMLDivElement; 217 | 218 | // create div and assign backgroundColor from color-picker to convert the hex-code to rgb format for the comparison 219 | let container = document.createElement("div"); 220 | container.style.backgroundColor = colorPicker.value; 221 | 222 | let wrapperBackgroundColor = wrapper.style.backgroundColor; // returns rgb format color 223 | let selectedBackgroundColor = container.style.backgroundColor; 224 | await waitFor(() => { 225 | expect(selectedBackgroundColor).toBe(wrapperBackgroundColor); 226 | }); 227 | }); 228 | 229 | describe("Gradients property", () => { 230 | it("renders wrapper with gradient equals to the gradient array", async () => { 231 | let renderedBackground = getEditorWrapperBg( 232 | "#eee811", 233 | ["#ba68c8", "#ffa7c4", "#e57373"], 234 | 140 235 | ); 236 | await waitFor(() => { 237 | expect(renderedBackground).toBe( 238 | `linear-gradient(140deg, #ba68c8, #ffa7c4, #e57373)` 239 | ); 240 | }); 241 | }); 242 | }); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /__tests__/components/snippng_control_header.test.tsx: -------------------------------------------------------------------------------- 1 | import { SnippngControlHeader } from "@/components"; 2 | import { SnippngEditorContext } from "@/context/SnippngEditorContext"; 3 | import { defaultEditorConfig, LANGUAGES, THEMES } from "@/lib/constants"; 4 | import { render, screen, waitFor } from "@testing-library/react"; 5 | import { act } from "react-dom/test-utils"; 6 | 7 | jest.mock("next/router", () => require("next-router-mock")); 8 | 9 | beforeAll(() => { 10 | document.createRange = () => { 11 | const range = new Range(); 12 | 13 | range.getBoundingClientRect = jest.fn(); 14 | 15 | range.getClientRects = () => { 16 | return { 17 | item: () => null, 18 | length: 0, 19 | [Symbol.iterator]: jest.fn(), 20 | }; 21 | }; 22 | 23 | return range; 24 | }; 25 | }); 26 | 27 | beforeEach(() => { 28 | // IntersectionObserver isn't available in test environment 29 | const mockIntersectionObserver = jest.fn(); 30 | mockIntersectionObserver.mockReturnValue({ 31 | observe: () => null, 32 | unobserve: () => null, 33 | disconnect: () => null, 34 | }); 35 | window.IntersectionObserver = mockIntersectionObserver; 36 | }); 37 | 38 | describe("SnippngControlHeader", () => { 39 | it("renders all CTA and inputs", async () => { 40 | await act(async () => { 41 | //@ts-ignore 42 | render(); 43 | }); 44 | await waitFor(() => { 45 | screen.getByTestId("wrapper-color-picker"); 46 | screen.getByTestId("settings-cta"); 47 | }); 48 | }); 49 | 50 | describe("Default theme and language", () => { 51 | it("renders with VS Code dark as a default theme", async () => { 52 | await act(async () => { 53 | render( 54 | language.id === "typescript") || 61 | LANGUAGES[0], 62 | selectedTheme: 63 | THEMES.find((theme) => theme.id === "vscodeDark") || 64 | THEMES[0], 65 | }, 66 | }} 67 | > 68 | {/* @ts-ignore */} 69 | 70 | 71 | ); 72 | }); 73 | await waitFor(() => { 74 | screen.getByText("VS Code Dark"); 75 | }); 76 | }); 77 | 78 | it("renders with TypeScript as a default language", async () => { 79 | await act(async () => { 80 | //@ts-ignore 81 | render(); 82 | }); 83 | await waitFor(() => { 84 | screen.getByText("TypeScript"); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /__tests__/components/snippng_window_controls.test.tsx: -------------------------------------------------------------------------------- 1 | import { SnippngWindowControls } from "@/components"; 2 | import { render, screen, waitFor } from "@testing-library/react"; 3 | import { act } from "react-dom/test-utils"; 4 | 5 | describe("SnippngWindowControls", () => { 6 | it("renders the editor window with left aligned mac controls", async () => { 7 | await act(async () => { 8 | render(); 9 | }); 10 | await waitFor(() => { 11 | screen.getByTestId("mac-left"); 12 | }); 13 | }); 14 | 15 | it("renders the editor window with right aligned mac controls", async () => { 16 | await act(async () => { 17 | render(); 18 | }); 19 | await waitFor(() => { 20 | screen.getByTestId("mac-right"); 21 | }); 22 | }); 23 | 24 | it("renders the editor window with left aligned window controls", async () => { 25 | await act(async () => { 26 | render(); 27 | }); 28 | await waitFor(() => { 29 | screen.getByTestId("windows-left"); 30 | }); 31 | }); 32 | 33 | it("renders the editor window with right aligned window controls", async () => { 34 | await act(async () => { 35 | render(); 36 | }); 37 | await waitFor(() => { 38 | screen.getByTestId("windows-right"); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from "@testing-library/react"; 2 | import { act } from "react-dom/test-utils"; 3 | import Home from "../pages/index"; 4 | 5 | beforeAll(() => { 6 | document.createRange = () => { 7 | const range = new Range(); 8 | 9 | range.getBoundingClientRect = jest.fn(); 10 | 11 | range.getClientRects = () => { 12 | return { 13 | item: () => null, 14 | length: 0, 15 | [Symbol.iterator]: jest.fn(), 16 | }; 17 | }; 18 | 19 | return range; 20 | }; 21 | }); 22 | 23 | jest.mock("next/router", () => require("next-router-mock")); 24 | 25 | describe("LandingPage", () => { 26 | it("renders the landing page", async () => { 27 | await act(async () => { 28 | render(); 29 | }); 30 | await waitFor(() => { 31 | screen.getByTestId("landing-container"); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/lib/color_picker.test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | rgbToHex, 3 | hexToRgb, 4 | getRgb, 5 | rgbToHsv, 6 | hsvToRgb, 7 | } from "@/lib/color-picker/utils"; 8 | 9 | describe("ColoPicker", () => { 10 | describe("Utility functions", () => { 11 | it("converts rgb color to hexcode color", async () => { 12 | const expectedHex = "#78881F".toLowerCase(); 13 | const convertedHex = rgbToHex({ r: 120, g: 136, b: 31 }).toLowerCase(); 14 | expect(expectedHex).toBe(convertedHex); 15 | }); 16 | 17 | it("converts hexcode color to rgb color", async () => { 18 | const expectedRGBObject = { r: 120, g: 136, b: 31 }; 19 | const convertedRGB = hexToRgb("#78881F"); 20 | expect(expectedRGBObject.r).toBe(convertedRGB.r); 21 | expect(expectedRGBObject.g).toBe(convertedRGB.g); 22 | expect(expectedRGBObject.b).toBe(convertedRGB.b); 23 | }); 24 | 25 | it("destructures RGB keys based on rgb string passed", async () => { 26 | const expectedRGBObject = { r: 120, g: 136, b: 31 }; 27 | const convertedRGB = getRgb("rgb(120, 136, 31)"); 28 | expect(expectedRGBObject.r).toBe(convertedRGB.r); 29 | expect(expectedRGBObject.g).toBe(convertedRGB.g); 30 | expect(expectedRGBObject.b).toBe(convertedRGB.b); 31 | }); 32 | 33 | it("converts rgb color to hsv color", async () => { 34 | const expectedHSV = { h: 69, s: 77.2, v: 53.3 }; 35 | const convertedHSV = rgbToHsv({ r: 120, g: 136, b: 31 }); 36 | expect(Math.floor(expectedHSV.h)).toBe(Math.floor(convertedHSV.h)); 37 | expect(Math.floor(expectedHSV.s)).toBe(Math.floor(convertedHSV.s)); 38 | expect(Math.floor(expectedHSV.v)).toBe(Math.floor(convertedHSV.v)); 39 | }); 40 | 41 | it("converts hsv color to rgb color", async () => { 42 | const expectedRGB = { r: 120, g: 136, b: 31 }; 43 | const convertedRGB = hsvToRgb({ h: 69, s: 77.2, v: 53.3 }); 44 | expect(Math.floor(expectedRGB.r)).toBe(Math.floor(convertedRGB.r)); 45 | expect(Math.floor(expectedRGB.g)).toBe(Math.floor(convertedRGB.g)); 46 | expect(Math.floor(expectedRGB.b)).toBe(Math.floor(convertedRGB.b)); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/utils.test.tsx: -------------------------------------------------------------------------------- 1 | import { DEFAULT_RANGES, DEFAULT_WIDTHS } from "@/lib/constants"; 2 | import { deepClone, validateSnippngConfig } from "@/utils"; 3 | 4 | const mockJSON = { 5 | code: "", 6 | snippetsName: "", 7 | editorFontSize: 16, 8 | editorWindowControlsType: "mac-left" as const, 9 | fileName: "@utils/debounce.ts", 10 | hasDropShadow: true, 11 | lineHeight: 19, 12 | paddingHorizontal: 70, 13 | paddingVertical: 70, 14 | rounded: true, 15 | selectedLang: { label: "TypeScript", id: "typescript" }, 16 | selectedTheme: { id: "tokyoNightStorm", label: "Tokyo Night Storm" }, 17 | showFileName: true, 18 | showLineNumbers: true, 19 | wrapperBg: "#eee811", 20 | gradients: ["#ba68c8", "#ffa7c4", "#e57373"], 21 | gradientAngle: 140, 22 | editorWidth: 450, 23 | bgImageVisiblePatch: null, 24 | bgBlur: 0, 25 | }; 26 | 27 | const inValidJSON = { 28 | invalidKey: "invalid value", 29 | }; 30 | 31 | beforeAll(() => { 32 | document.createRange = () => { 33 | const range = new Range(); 34 | 35 | range.getBoundingClientRect = jest.fn(); 36 | 37 | range.getClientRects = () => { 38 | return { 39 | item: () => null, 40 | length: 0, 41 | [Symbol.iterator]: jest.fn(), 42 | }; 43 | }; 44 | 45 | return range; 46 | }; 47 | }); 48 | 49 | describe("Utils", () => { 50 | it("deep clones the javascript object", async () => { 51 | let date = new Date().toISOString(); 52 | let updatedDate = new Date().setFullYear(2002); 53 | 54 | const objectToBeCloned = { 55 | name: "John", 56 | age: 20, 57 | marks: { 58 | science: 70, 59 | math: 75, 60 | }, 61 | birthDate: date, 62 | }; 63 | const clonedObject = deepClone(objectToBeCloned); 64 | clonedObject.name = "Updated"; 65 | clonedObject.marks.science = 10; 66 | clonedObject.birthDate = updatedDate; 67 | 68 | expect(objectToBeCloned.name).toBe("John"); 69 | expect(objectToBeCloned.marks.science).toBe(70); 70 | expect(objectToBeCloned.birthDate).toBe(date); 71 | 72 | expect(clonedObject.name).toBe("Updated"); 73 | expect(clonedObject.marks.science).toBe(10); 74 | expect(clonedObject.birthDate).toBe(updatedDate); 75 | }); 76 | 77 | it("validates editor config coming from uploaded JSON file", async () => { 78 | let minValues = DEFAULT_RANGES.min; 79 | let maxValues = DEFAULT_RANGES.max; 80 | const mockOneResult = validateSnippngConfig({ ...mockJSON }); 81 | 82 | const mockMissingKeyResult = validateSnippngConfig({ 83 | ...mockJSON, 84 | selectedLang: undefined as any, 85 | }); 86 | 87 | const mockInvalidKeyResult = validateSnippngConfig({ 88 | ...mockJSON, 89 | selectedLang: [] as any, 90 | }); 91 | 92 | const mockBlueCheckResult = validateSnippngConfig({ 93 | ...mockJSON, 94 | bgBlur: 100, 95 | }); 96 | const mockLineHeightResult = validateSnippngConfig({ 97 | ...mockJSON, 98 | lineHeight: 100, 99 | }); 100 | const mockPadHorResult = validateSnippngConfig({ 101 | ...mockJSON, 102 | paddingHorizontal: 200, 103 | }); 104 | const mockPadVerResult = validateSnippngConfig({ 105 | ...mockJSON, 106 | paddingVertical: 200, 107 | }); 108 | const mockFontSizeResult = validateSnippngConfig({ 109 | ...mockJSON, 110 | editorFontSize: 54, 111 | }); 112 | const mockGradAngResult = validateSnippngConfig({ 113 | ...mockJSON, 114 | gradientAngle: 361, 115 | }); 116 | const mockEdWidthMinResult = validateSnippngConfig({ 117 | ...mockJSON, 118 | editorWidth: -10, 119 | }); 120 | const mockEdWidthMaxResult = validateSnippngConfig({ 121 | ...mockJSON, 122 | editorWidth: 3000, 123 | }); 124 | const invalidResult = validateSnippngConfig({ ...(inValidJSON as any) }); 125 | expect(mockOneResult).toBe(""); 126 | expect(mockMissingKeyResult).toBe("value.selectedLang is missing"); 127 | expect(mockInvalidKeyResult).toContain("missing"); 128 | expect(mockBlueCheckResult).toBe( 129 | `bgBlur value must be in the range ${minValues.BLUR} to ${maxValues.BLUR}` 130 | ); 131 | expect(mockLineHeightResult).toBe( 132 | `lineHeight value must be in the range ${minValues.LINE_HEIGHT} to ${maxValues.LINE_HEIGHT}` 133 | ); 134 | expect(mockPadHorResult).toBe( 135 | `paddingHorizontal value must be in the range ${minValues.PADDING_HORIZONTAL} to ${maxValues.PADDING_HORIZONTAL}` 136 | ); 137 | expect(mockPadVerResult).toBe( 138 | `paddingVertical value must be in the range ${minValues.PADDING_VERTICAL} to ${maxValues.PADDING_VERTICAL}` 139 | ); 140 | expect(mockFontSizeResult).toBe( 141 | `editorFontSize value must be in the range ${minValues.FONT_SIZE} to ${maxValues.FONT_SIZE}` 142 | ); 143 | expect(mockGradAngResult).toBe( 144 | `gradientAngle value must be in the range ${minValues.GRADIENT_ANGLE} to ${maxValues.GRADIENT_ANGLE}` 145 | ); 146 | expect(mockEdWidthMinResult).toBe( 147 | `editorWidth value must be in the range ${DEFAULT_WIDTHS.minWidth} to ${DEFAULT_WIDTHS.maxWidth}` 148 | ); 149 | expect(mockEdWidthMaxResult).toBe( 150 | `editorWidth value must be in the range ${DEFAULT_WIDTHS.minWidth} to ${DEFAULT_WIDTHS.maxWidth}` 151 | ); 152 | expect(invalidResult).toBeTruthy(); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /components/ErrorText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button, { SnippngButtonType } from "./form/Button"; 3 | import { FolderPlusIcon } from "@heroicons/react/24/outline"; 4 | 5 | interface Props { 6 | errorTitle: string; 7 | errorSubTitle?: string; 8 | errorActionProps?: SnippngButtonType; 9 | ErrorIcon?: ((props: React.SVGProps) => JSX.Element) | null; 10 | } 11 | 12 | const ErrorText: React.FC = ({ 13 | errorTitle, 14 | errorSubTitle, 15 | errorActionProps, 16 | ErrorIcon, 17 | }) => { 18 | return ( 19 |
20 | {ErrorIcon ? ( 21 | 22 | ) : ( 23 | 24 | )} 25 |

26 | {errorTitle} 27 |

28 | {errorSubTitle ? ( 29 |

30 | {errorSubTitle} 31 |

32 | ) : null} 33 | {errorActionProps ? ( 34 |
35 | 36 |
37 | ) : null} 38 |
39 | ); 40 | }; 41 | 42 | export default ErrorText; 43 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 |
7 | 8 | 12 | 16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Loader; 23 | -------------------------------------------------------------------------------- /components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from "@/utils"; 2 | import { CommandLineIcon } from "@heroicons/react/24/outline"; 3 | import React from "react"; 4 | 5 | const isBeta = false; 6 | 7 | const Logo: React.FC<{ size?: "xs" | "sm" | "xl" | "2xl" }> = ({ 8 | size = "xl", 9 | }) => { 10 | const getTextClassesBySize = () => { 11 | switch (size) { 12 | case "xs": 13 | return " text-xs"; 14 | case "sm": 15 | return " text-sm"; 16 | case "xl": 17 | return " text-xl"; 18 | case "2xl": 19 | return " text-2xl"; 20 | default: 21 | return "text-xl"; 22 | } 23 | }; 24 | 25 | const getIconClassesBySize = () => { 26 | switch (size) { 27 | case "xs": 28 | return "h-4 w-4 mr-1"; 29 | case "sm": 30 | return "h-5 w-5 mr-1"; 31 | case "xl": 32 | return "h-7 w-7 mr-2"; 33 | case "2xl": 34 | return "h-9 w-9 mr-2"; 35 | default: 36 | return "h-7 w-7 mr-2"; 37 | } 38 | }; 39 | 40 | return ( 41 |

47 | 50 | 51 | Snippng 52 | {isBeta ? ( 53 | 54 | Beta 55 | 56 | ) : null} 57 | 58 |

59 | ); 60 | }; 61 | 62 | export default Logo; 63 | -------------------------------------------------------------------------------- /components/NoSSRWrapper.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import React from "react"; 3 | 4 | const NoSSRWrapper: React.FC<{ children: React.ReactNode }> = (props) => { 5 | return {props.children}; 6 | }; 7 | 8 | export default dynamic(() => Promise.resolve(NoSSRWrapper), { 9 | ssr: false, 10 | }); 11 | -------------------------------------------------------------------------------- /components/SigninButton.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from "@/context/AuthContext"; 2 | import React from "react"; 3 | import Button from "./form/Button"; 4 | import GoogleIcon from "./icons/GoogleIcon"; 5 | 6 | const SigninButton = () => { 7 | const { loginWithGoogle } = useAuth(); 8 | 9 | return ( 10 | <> 11 | 19 | 20 | ); 21 | }; 22 | 23 | export default SigninButton; 24 | -------------------------------------------------------------------------------- /components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; 2 | 3 | const ThemeToggle = () => { 4 | const disableTransitionsTemporarily = () => { 5 | document.documentElement.classList.add("[&_*]:!transition-none"); 6 | window.setTimeout(() => { 7 | document.documentElement.classList.remove("[&_*]:!transition-none"); 8 | }, 0); 9 | }; 10 | 11 | const toggleMode = () => { 12 | disableTransitionsTemporarily(); 13 | let isDarkMode = document.documentElement.classList.toggle("dark"); 14 | window.localStorage.isDarkMode = isDarkMode; 15 | }; 16 | 17 | return ( 18 | 28 | ); 29 | }; 30 | 31 | export default ThemeToggle; 32 | -------------------------------------------------------------------------------- /components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { ToastInterface } from "@/types"; 2 | import { Transition } from "@headlessui/react"; 3 | import { XMarkIcon } from "@heroicons/react/20/solid"; 4 | import { 5 | CheckCircleIcon, 6 | InformationCircleIcon, 7 | XCircleIcon, 8 | } from "@heroicons/react/24/outline"; 9 | import React, { Fragment, useEffect, useState } from "react"; 10 | 11 | interface Props extends ToastInterface { 12 | onClose: () => void; 13 | } 14 | 15 | const Toast: React.FC = ({ 16 | message, 17 | description, 18 | type = "success", 19 | onClose, 20 | }) => { 21 | const [show, setShow] = useState(false); 22 | 23 | const getIconByType = () => { 24 | switch (type) { 25 | case "success": 26 | return ( 27 |