From bad4ce58503fb52d5ceff0dbecb535dfcf132731 Mon Sep 17 00:00:00 2001 From: wajeshubham Date: Mon, 6 Feb 2023 00:45:34 +0530 Subject: [PATCH 01/12] Add editor config import export sidebar --- __tests__/auth.test.tsx | 11 + .../components/snippng_code_area.test.tsx | 13 + .../snippng_control_header.test.tsx | 14 + __tests__/utils.test.tsx | 30 + components/editor/SnippngCodeArea.tsx | 10 +- .../editor/SnippngConfigImportExporter.tsx | 225 +++++++ components/editor/SnippngControlHeader.tsx | 632 +++++++++--------- components/index.tsx | 25 +- config/firebase.ts | 2 +- styles/globals.css | 13 +- types/editor.ts | 7 + utils/index.ts | 37 + 12 files changed, 701 insertions(+), 318 deletions(-) create mode 100644 __tests__/utils.test.tsx create mode 100644 components/editor/SnippngConfigImportExporter.tsx diff --git a/__tests__/auth.test.tsx b/__tests__/auth.test.tsx index c001e04..74e59a3 100644 --- a/__tests__/auth.test.tsx +++ b/__tests__/auth.test.tsx @@ -3,6 +3,17 @@ import Home from "@/pages"; import { render, screen, waitFor } from "@testing-library/react"; import { act } from "react-dom/test-utils"; +beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; +}); + beforeAll(() => { document.createRange = () => { const range = new Range(); diff --git a/__tests__/components/snippng_code_area.test.tsx b/__tests__/components/snippng_code_area.test.tsx index 3e861d8..59a6abd 100644 --- a/__tests__/components/snippng_code_area.test.tsx +++ b/__tests__/components/snippng_code_area.test.tsx @@ -4,6 +4,17 @@ import { defaultEditorConfig } from "@/lib/constants"; import { render, screen, waitFor } from "@testing-library/react"; import { act } from "react-dom/test-utils"; +beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; +}); + beforeAll(() => { document.createRange = () => { const range = new Range(); @@ -192,6 +203,8 @@ describe("SnippngCodeArea", () => { ); + + // @ts-ignore render(); }); const colorPicker = document.getElementById( diff --git a/__tests__/components/snippng_control_header.test.tsx b/__tests__/components/snippng_control_header.test.tsx index 6bf8905..ce9cce9 100644 --- a/__tests__/components/snippng_control_header.test.tsx +++ b/__tests__/components/snippng_control_header.test.tsx @@ -22,9 +22,21 @@ beforeAll(() => { }; }); +beforeEach(() => { + // IntersectionObserver isn't available in test environment + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; +}); + describe("SnippngControlHeader", () => { it("renders all CTA and inputs", async () => { await act(async () => { + //@ts-ignore render(); }); await waitFor(() => { @@ -51,6 +63,7 @@ describe("SnippngControlHeader", () => { }, }} > + {/* @ts-ignore */} ); @@ -62,6 +75,7 @@ describe("SnippngControlHeader", () => { it("renders with TypeScript as a default language", async () => { await act(async () => { + //@ts-ignore render(); }); await waitFor(() => { diff --git a/__tests__/utils.test.tsx b/__tests__/utils.test.tsx new file mode 100644 index 0000000..6b9a8b3 --- /dev/null +++ b/__tests__/utils.test.tsx @@ -0,0 +1,30 @@ +import { deepClone } from "@/utils"; + +describe("Utils", () => { + it("deep clones the javascript object", async () => { + let date = new Date().toISOString(); + let updatedDate = new Date().setFullYear(2002); + + const objectToBeCloned = { + name: "John", + age: 20, + marks: { + science: 70, + math: 75, + }, + birthDate: date, + }; + const clonedObject = deepClone(objectToBeCloned); + clonedObject.name = "Updated"; + clonedObject.marks.science = 10; + clonedObject.birthDate = updatedDate; + + expect(objectToBeCloned.name).toBe("John"); + expect(objectToBeCloned.marks.science).toBe(70); + expect(objectToBeCloned.birthDate).toBe(date); + + expect(clonedObject.name).toBe("Updated"); + expect(clonedObject.marks.science).toBe(10); + expect(clonedObject.birthDate).toBe(updatedDate); + }); +}); diff --git a/components/editor/SnippngCodeArea.tsx b/components/editor/SnippngCodeArea.tsx index 1425356..944292b 100644 --- a/components/editor/SnippngCodeArea.tsx +++ b/components/editor/SnippngCodeArea.tsx @@ -1,7 +1,13 @@ import { useRef, useState } from "react"; import { DEFAULT_BASE_SETUP } from "@/lib/constants"; -import { clsx, getEditorWrapperBg, getLanguage, getTheme } from "@/utils"; +import { + clsx, + deepClone, + getEditorWrapperBg, + getLanguage, + getTheme, +} from "@/utils"; import { langs, loadLanguage } from "@uiw/codemirror-extensions-langs"; import * as themes from "@uiw/codemirror-themes-all"; @@ -63,7 +69,7 @@ const SnippngCodeArea = () => { if (!user) return; setSaving(true); try { - const dataToBeAdded = { ...structuredClone(editorConfig) }; // deep clone the editor config to avoid mutation + const dataToBeAdded = { ...deepClone(editorConfig) }; // deep clone the editor config to avoid mutation delete dataToBeAdded.uid; // delete existing uid if exists const savedDoc = await addDoc(collection(db, "snippets"), { ...dataToBeAdded, diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx new file mode 100644 index 0000000..db62e68 --- /dev/null +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -0,0 +1,225 @@ +import { Fragment, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +import { ClipboardDocumentIcon, XMarkIcon } from "@heroicons/react/24/outline"; + +import { useSnippngEditor } from "@/context/SnippngEditorContext"; +import { clsx } from "@/utils"; +import { useToast } from "@/context/ToastContext"; + +interface Props { + open: boolean; + onClose: () => void; +} + +const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { + const { editorConfig } = useSnippngEditor(); + const [isExport, setIsExport] = useState(true); + + const { addToast } = useToast(); + + const getEditorConfigJsx = ( + object: object | Array, + isArray: boolean = false + ) => { + if (object === null) return "null"; + return Object.keys(object) + .sort() + .map((key) => { + if (key === "code") return; + const val = object[key as keyof typeof object] as + | string + | number + | boolean + | { [key: string]: string | number }; + return ( +
+            
+              {isArray ? "" : <>"{key}": }
+            
+
+            {val && typeof val === "object" ? (
+              <>
+                {Array.isArray(val) ? "[" : "{"}
+                {getEditorConfigJsx(val, Array.isArray(val))}
+                {Array.isArray(val) ? "]," : "},"}
+              
+            ) : (
+              <>
+                {typeof val === "string" ? (
+                  
+                    {`"${val}"` || <>""}
+                  
+                ) : typeof val === "number" ? (
+                  
+                    {+val}
+                  
+                ) : typeof val === "boolean" ? (
+                  
+                    {val?.toString()}
+                  
+                ) : (
+                  null
+                )}
+                ,
+                
+ + )} +
+ ); + }); + }; + + return ( + + + +
+ + +
+
+
+ + +
+
+
+
+ + Import/export config + +
+ +
+
+
+

+ You can copy/export the config in a json file which + you can share to others. Similarly, you can + import/paste the config here to apply it on the code + area +

+
+
+
+
+ + +
+
+
+
+
+
+ +

+ Paste your config and click import to apply + changes or Export the following config by + clicking export button +

+ +
+ +
+                                  {"{"}
+                                  {getEditorConfigJsx({
+                                    ...editorConfig,
+                                  })}
+                                  {"}"}
+                                
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default SnippngConfigImportExporter; diff --git a/components/editor/SnippngControlHeader.tsx b/components/editor/SnippngControlHeader.tsx index e0ab169..04accb7 100644 --- a/components/editor/SnippngControlHeader.tsx +++ b/components/editor/SnippngControlHeader.tsx @@ -12,6 +12,7 @@ import { SelectOptionInterface } from "@/types"; import { getEditorWrapperBg } from "@/utils"; import { Menu, Transition } from "@headlessui/react"; import { + ArrowsUpDownIcon, ChevronDownIcon, CloudArrowDownIcon, Cog6ToothIcon, @@ -21,15 +22,18 @@ import { SparklesIcon, } from "@heroicons/react/24/outline"; import * as htmlToImage from "html-to-image"; -import { Fragment, RefObject } from "react"; +import { Fragment, RefObject, useState } from "react"; import Button from "../form/Button"; import Checkbox from "../form/Checkbox"; import Range from "../form/Range"; import Select from "../form/Select"; +import SnippngConfigImportExporter from "./SnippngConfigImportExporter"; const SnippngControlHeader: React.FC<{ wrapperRef: RefObject; }> = ({ wrapperRef }) => { + const [openImportExportSidebar, setOpenImportExportSidebar] = useState(false); + const { editorConfig, handleConfigChange } = useSnippngEditor(); const { addToast } = useToast(); @@ -89,320 +93,342 @@ const SnippngControlHeader: React.FC<{ }; return ( -
- { - if (!val.id) return; - handleConfigChange("selectedTheme")(val); + <> + {/* headless ui renders this sidebar in portal */} + { + setOpenImportExportSidebar(false); }} - options={[...THEMES]} /> - + + { + if (!val.id) return; + handleConfigChange("selectedLang")(val); }} + options={[...LANGUAGES]} + /> +
- - - handleConfigChange("bgImageVisiblePatch")(src)} - > - - -
- -
- -
- - - -
- - - -
- { - handleConfigChange("showLineNumbers")(!showLineNumbers); - }} - /> -
-
- { - handleConfigChange("hasDropShadow")(!hasDropShadow); - }} - /> -
-
- { - handleConfigChange("rounded")(!rounded); - }} - /> -
-
- { - handleConfigChange("showFileName")(!showFileName); - }} + + + { + let colors = [...gradients, color]; + handleConfigChange("gradients")(colors); + }} + onGradientUnSelect={(color) => { + let colors = [...gradients].filter((c) => c !== color); + handleConfigChange("gradients")(colors); + }} + onChange={(color) => { + handleConfigChange("wrapperBg")(color); + }} + > + + + handleConfigChange("bgImageVisiblePatch")(src)} + > + + +
+ +
+ +
+ + +
-
- { - if (!e.target.checked) { - handleConfigChange("bgImageVisiblePatch")( - defaultEditorConfig.bgImageVisiblePatch + +
+ + + +
+ { + handleConfigChange("showLineNumbers")(!showLineNumbers); + }} + /> +
+
+ { + handleConfigChange("hasDropShadow")(!hasDropShadow); + }} + /> +
+
+ { + handleConfigChange("rounded")(!rounded); + }} + /> +
+
+ { + handleConfigChange("showFileName")(!showFileName); + }} + /> +
+
+ { + if (!e.target.checked) { + handleConfigChange("bgImageVisiblePatch")( + defaultEditorConfig.bgImageVisiblePatch + ); + handleConfigChange("bgBlur")( + defaultEditorConfig.bgBlur + ); + handleConfigChange("gradients")( + defaultEditorConfig.gradients + ); + handleConfigChange("wrapperBg")( + defaultEditorConfig.wrapperBg + ); + } else { + handleConfigChange("bgImageVisiblePatch")(null); + handleConfigChange("bgBlur")(0); + handleConfigChange("gradients")([]); + handleConfigChange("wrapperBg")("transparent"); + } + }} + /> +
+
+ { + handleConfigChange("bgBlur")(+e.target.value); + }} + /> +
+
+ { + handleConfigChange("editorFontSize")(+e.target.value); + }} + max={32} + min={10} + rangeMin={"10px"} + rangeMax={"32px"} + /> +
+
+ { + handleConfigChange("paddingVertical")(+e.target.value); + }} + max={100} + min={0} + rangeMin={"0px"} + rangeMax={"100px"} + /> +
+
+ { + handleConfigChange("paddingHorizontal")(+e.target.value); + }} + max={100} + min={0} + rangeMin={"0px"} + rangeMax={"100px"} + /> +
+
+ { + handleConfigChange("lineHeight")(+e.target.value); + }} + max={40} + min={10} + rangeMin={"10px"} + rangeMax={"40px"} + /> +
+
+ { + handleConfigChange("gradientAngle")(+e.target.value); + }} + max={360} + min={0} + rangeMin={"0deg"} + rangeMax={"360deg"} + /> +
+
+

Window controls type

+ { + handleConfigChange("editorWindowControlsType")( + "mac-left" ); - handleConfigChange("bgBlur")(defaultEditorConfig.bgBlur); - handleConfigChange("gradients")( - defaultEditorConfig.gradients + }} + /> + { + handleConfigChange("editorWindowControlsType")( + "mac-right" ); - handleConfigChange("wrapperBg")( - defaultEditorConfig.wrapperBg + }} + /> + { + handleConfigChange("editorWindowControlsType")( + "windows-left" ); - } else { - handleConfigChange("bgImageVisiblePatch")(null); - handleConfigChange("bgBlur")(0); - handleConfigChange("gradients")([]); - handleConfigChange("wrapperBg")("transparent"); - } - }} - /> -
-
- { - handleConfigChange("bgBlur")(+e.target.value); - }} - /> -
-
- { - handleConfigChange("editorFontSize")(+e.target.value); - }} - max={32} - min={10} - rangeMin={"10px"} - rangeMax={"32px"} - /> -
-
- { - handleConfigChange("paddingVertical")(+e.target.value); - }} - max={100} - min={0} - rangeMin={"0px"} - rangeMax={"100px"} - /> -
-
- { - handleConfigChange("paddingHorizontal")(+e.target.value); - }} - max={100} - min={0} - rangeMin={"0px"} - rangeMax={"100px"} - /> -
-
- { - handleConfigChange("lineHeight")(+e.target.value); - }} - max={40} - min={10} - rangeMin={"10px"} - rangeMax={"40px"} - /> -
-
- { - handleConfigChange("gradientAngle")(+e.target.value); - }} - max={360} - min={0} - rangeMin={"0deg"} - rangeMax={"360deg"} - /> -
-
-

Window controls type

- { - handleConfigChange("editorWindowControlsType")("mac-left"); - }} - /> - { - handleConfigChange("editorWindowControlsType")("mac-right"); - }} - /> - { - handleConfigChange("editorWindowControlsType")( - "windows-left" - ); - }} - /> - { - handleConfigChange("editorWindowControlsType")( - "windows-right" - ); - }} - /> -
-
-
-
+ }} + /> + { + handleConfigChange("editorWindowControlsType")( + "windows-right" + ); + }} + /> +
+
+
+
+
+
- + ); }; diff --git a/components/index.tsx b/components/index.tsx index 54fae72..c25a4d3 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -1,4 +1,5 @@ import SnippngCodeArea from "./editor/SnippngCodeArea"; +import SnippngConfigImportExporter from "./editor/SnippngConfigImportExporter"; import SnippngControlHeader from "./editor/SnippngControlHeader"; import SnippngWindowControls from "./editor/SnippngWindowControls"; import ErrorText from "./ErrorText"; @@ -13,20 +14,22 @@ import NoSSRWrapper from "./NoSSRWrapper"; import SigninButton from "./SigninButton"; import ThemeToggle from "./ThemeToggle"; import Toast from "./Toast"; + export { - Toast, - ThemeToggle, - SnippngCodeArea, - NoSSRWrapper, - SnippngWindowControls, Button, - Select, - Range, Checkbox, - SnippngControlHeader, - Logo, ErrorText, - SigninButton, - Loader, Input, + Loader, + Logo, + NoSSRWrapper, + Range, + Select, + SigninButton, + SnippngCodeArea, + SnippngControlHeader, + SnippngConfigImportExporter, + SnippngWindowControls, + ThemeToggle, + Toast, }; diff --git a/config/firebase.ts b/config/firebase.ts index 561b7a2..7065628 100644 --- a/config/firebase.ts +++ b/config/firebase.ts @@ -1,7 +1,7 @@ import { initializeApp } from "firebase/app"; import { Firestore, getFirestore } from "firebase/firestore"; import { Auth, getAuth } from "firebase/auth"; -import { Analytics, getAnalytics } from "firebase/analytics"; +import { Analytics, getAnalytics, isSupported } from "firebase/analytics"; let auth: Auth | undefined; let db: Firestore | undefined; diff --git a/styles/globals.css b/styles/globals.css index b2c6916..8156e38 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -20,17 +20,28 @@ body { @apply pt-8; } +.CodeMirror__Config__Editor { + @apply relative text-lg; +} + +.CodeMirror__Config__Editor div { + @apply block px-1 pb-1 whitespace-pre-wrap break-normal break-words; +} + .cm-scroller { position: relative; overflow: hidden !important; } .cm-content { - border-right-width: 70px; border-color: transparent; max-width: 100%; } +.CodeMirror__Main__Editor .cm-content { + border-right-width: 70px; +} + .cm-gutters { min-height: 100%; z-index: 3; diff --git a/types/editor.ts b/types/editor.ts index 545d0b5..284a23d 100644 --- a/types/editor.ts +++ b/types/editor.ts @@ -31,6 +31,13 @@ export interface SnippngEditorConfigInterface { bgBlur: number; } +type CustomOmit = Omit; // Omit not throwing type error for some reason + +export type SnippngExportableConfig = CustomOmit< + SnippngEditorConfigInterface, + "uid" | "ownerUid" +>; + export interface SnippngEditorContextInterface { editorConfig: SnippngEditorConfigInterface; handleConfigChange: < diff --git a/utils/index.ts b/utils/index.ts index 226865d..ae64bd8 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,3 +1,4 @@ +import { SnippngEditorConfigInterface, SnippngExportableConfig } from "@/types"; import { langs } from "@uiw/codemirror-extensions-langs"; import * as themes from "@uiw/codemirror-themes-all"; @@ -23,3 +24,39 @@ export const getEditorWrapperBg = ( ? selectedGradients[0] : `linear-gradient(${angle}deg, ${selectedGradients.join(", ")})`; }; + +export const deepClone = (obj: T) => { + if (typeof obj !== "object" || obj === null) { + return obj; + } + + if (obj instanceof Date) { + return new Date(obj.getTime()); + } + + if (obj instanceof Array) { + return obj.reduce((arr, item, i) => { + arr[i] = deepClone(item); + return arr; + }, []); + } + + if (obj instanceof Object) { + return Object.keys(obj).reduce((newObj: object, key) => { + // @ts-ignore + newObj[key] = deepClone(obj[key]); + return newObj; + }, {}); + } +}; + +export const getExportableConfig = ( + editorConfig: SnippngEditorConfigInterface +): SnippngExportableConfig => { + const deepClonedConfig = { ...deepClone(editorConfig) }; + deepClonedConfig.code = ""; + delete deepClonedConfig.uid; + delete deepClonedConfig.ownerUid; + const exportableConfig: SnippngExportableConfig = deepClonedConfig; + return exportableConfig; +}; From 491244b9070dfc1b1ac8c73d06d9f37baa8b4a02 Mon Sep 17 00:00:00 2001 From: wajeshubham Date: Wed, 8 Feb 2023 01:41:43 +0530 Subject: [PATCH 02/12] Add functionality to export/copy/download teh current editor cofig --- .../editor/SnippngConfigImportExporter.tsx | 169 +++++++++++------- components/editor/SnippngControlHeader.tsx | 15 +- utils/index.ts | 4 + 3 files changed, 121 insertions(+), 67 deletions(-) diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index db62e68..8f81f8e 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -1,10 +1,14 @@ -import { Fragment, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; -import { ClipboardDocumentIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { + ClipboardDocumentIcon, + CloudArrowDownIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { Fragment, useState } from "react"; import { useSnippngEditor } from "@/context/SnippngEditorContext"; -import { clsx } from "@/utils"; import { useToast } from "@/context/ToastContext"; +import { clsx, copyJSONText, getExportableConfig } from "@/utils"; interface Props { open: boolean; @@ -17,6 +21,29 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { const { addToast } = useToast(); + const getJsxByDatatype = (value: string | number | boolean) => { + switch (typeof value) { + case "string": + return ( + + {`"${value}"` || <>""} + + ); + case "number": + return ( + {+value} + ); + case "boolean": + return ( + + {value.toString()} + + ); + default: + return null; + } + }; + const getEditorConfigJsx = ( object: object | Array, isArray: boolean = false @@ -26,11 +53,13 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { .sort() .map((key) => { if (key === "code") return; + const val = object[key as keyof typeof object] as | string | number | boolean | { [key: string]: string | number }; + return (
 = ({ open, onClose }) => {
               
             ) : (
               <>
-                {typeof val === "string" ? (
-                  
-                    {`"${val}"` || <>""}
-                  
-                ) : typeof val === "number" ? (
-                  
-                    {+val}
-                  
-                ) : typeof val === "boolean" ? (
-                  
-                    {val?.toString()}
-                  
-                ) : (
-                  null
-                )}
+                {getJsxByDatatype(val)}
                 ,
                 
@@ -74,7 +89,7 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { return ( - + = ({ open, onClose }) => {
-
- -

- Paste your config and click import to apply - changes or Export the following config by - clicking export button -

- -
- -
-                                  {"{"}
-                                  {getEditorConfigJsx({
-                                    ...editorConfig,
-                                  })}
-                                  {"}"}
-                                
+ Current config + +

+ Paste your config and click import to apply + changes or Export the following config by + clicking export button +

+ +
+
+ + +
+
+                                    {"{"}
+                                    {getEditorConfigJsx({
+                                      ...editorConfig,
+                                    })}
+                                    {"}"}
+                                  
+
-
+ ) : ( + <> +

Import config

+ + )}
diff --git a/components/editor/SnippngControlHeader.tsx b/components/editor/SnippngControlHeader.tsx index 04accb7..937a397 100644 --- a/components/editor/SnippngControlHeader.tsx +++ b/components/editor/SnippngControlHeader.tsx @@ -213,6 +213,15 @@ const SnippngControlHeader: React.FC<{ leaveTo="transform opacity-0 scale-95" > +
- ); diff --git a/utils/index.ts b/utils/index.ts index ae64bd8..e55fdb1 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -60,3 +60,7 @@ export const getExportableConfig = ( const exportableConfig: SnippngExportableConfig = deepClonedConfig; return exportableConfig; }; + +export const copyJSONText = async (data: T) => { + return navigator.clipboard?.writeText(JSON.stringify(data)); +}; From a70f500409dc0ccf034662b76413ac38f9892804 Mon Sep 17 00:00:00 2001 From: wajeshubham Date: Wed, 8 Feb 2023 01:52:22 +0530 Subject: [PATCH 03/12] Add code field in config with empty value --- .../editor/SnippngConfigImportExporter.tsx | 25 ++++++++----------- utils/index.ts | 3 ++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index 8f81f8e..aecfc0d 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -52,14 +52,7 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { return Object.keys(object) .sort() .map((key) => { - if (key === "code") return; - - const val = object[key as keyof typeof object] as - | string - | number - | boolean - | { [key: string]: string | number }; - + const val = object[key as keyof typeof object]; return (
 = ({ open, onClose }) => {
             
               {isArray ? "" : <>"{key}": }
             
-
             {val && typeof val === "object" ? (
               <>
                 {Array.isArray(val) ? "[" : "{"}
@@ -77,8 +69,7 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => {
               
             ) : (
               <>
-                {getJsxByDatatype(val)}
-                ,
+                {getJsxByDatatype(val)},
                 
)} @@ -242,12 +233,18 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => {
                                     {"{"}
-                                    {getEditorConfigJsx({
-                                      ...editorConfig,
-                                    })}
+                                    {getEditorConfigJsx(
+                                      getExportableConfig({
+                                        ...editorConfig,
+                                      })
+                                    )}
                                     {"}"}
                                   
+ + code and background image are empty while + exporting + ) : ( <> diff --git a/utils/index.ts b/utils/index.ts index e55fdb1..035254e 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -25,7 +25,7 @@ export const getEditorWrapperBg = ( : `linear-gradient(${angle}deg, ${selectedGradients.join(", ")})`; }; -export const deepClone = (obj: T) => { +export const deepClone = (obj: T): T | any => { if (typeof obj !== "object" || obj === null) { return obj; } @@ -55,6 +55,7 @@ export const getExportableConfig = ( ): SnippngExportableConfig => { const deepClonedConfig = { ...deepClone(editorConfig) }; deepClonedConfig.code = ""; + deepClonedConfig.bgImageVisiblePatch = null; delete deepClonedConfig.uid; delete deepClonedConfig.ownerUid; const exportableConfig: SnippngExportableConfig = deepClonedConfig; From 4aba19b000111ae5692799e98fa87edab360af8f Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Thu, 9 Feb 2023 13:17:46 +0530 Subject: [PATCH 04/12] Add json file accept with basic imported config validation --- .../editor/SnippngConfigImportExporter.tsx | 64 +++++++++++++++++-- utils/index.ts | 10 +++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index aecfc0d..be323ad 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -1,14 +1,23 @@ import { Dialog, Transition } from "@headlessui/react"; import { + ArrowDownTrayIcon, ClipboardDocumentIcon, CloudArrowDownIcon, XMarkIcon, } from "@heroicons/react/24/outline"; -import { Fragment, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { useSnippngEditor } from "@/context/SnippngEditorContext"; import { useToast } from "@/context/ToastContext"; -import { clsx, copyJSONText, getExportableConfig } from "@/utils"; +import { + clsx, + copyJSONText, + deepClone, + getExportableConfig, + validateSnippngConfig, +} from "@/utils"; +import Button from "../form/Button"; +import { SnippngEditorConfigInterface } from "@/types"; interface Props { open: boolean; @@ -18,7 +27,9 @@ interface Props { const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { const { editorConfig } = useSnippngEditor(); const [isExport, setIsExport] = useState(true); - + const [configToBeImport, setConfigToBeImport] = + useState(null); + const [isConfigValid, setIsConfigValid] = useState(false); const { addToast } = useToast(); const getJsxByDatatype = (value: string | number | boolean) => { @@ -78,6 +89,23 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { }); }; + const parseAndValidateImportedJson = ( + e: React.ChangeEvent + ) => { + if (!e.target.files) return; + const file = e.target.files[0]; + let reader = new FileReader(); + reader.addEventListener("load", (e) => { + let jsonData = JSON.parse(e.target!.result! as string); + setConfigToBeImport(jsonData); + setIsConfigValid(validateSnippngConfig(jsonData)); + }); + reader.readAsText(file); + e.target.value = ""; + e.target.files = null; + reader.removeEventListener("load", () => {}); + }; + return ( @@ -248,7 +276,35 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { ) : ( <> -

Import config

+ + + {configToBeImport ? ( +
+ {!isConfigValid ? ( + + Imported config is invalid + + ) : null} +
+
+                                        {"{"}
+                                        {getEditorConfigJsx({
+                                          ...deepClone(configToBeImport),
+                                        })}
+                                        {"}"}
+                                      
+
+
+ ) : null} )} diff --git a/utils/index.ts b/utils/index.ts index 035254e..aa60d2f 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,6 +1,7 @@ import { SnippngEditorConfigInterface, SnippngExportableConfig } from "@/types"; import { langs } from "@uiw/codemirror-extensions-langs"; import * as themes from "@uiw/codemirror-themes-all"; +import { defaultEditorConfig } from "@/lib/constants"; export const clsx = (...classNames: string[]) => classNames.filter(Boolean).join(" "); @@ -62,6 +63,15 @@ export const getExportableConfig = ( return exportableConfig; }; +export const validateSnippngConfig = (config: SnippngEditorConfigInterface) => { + return Object.keys(config).every((key) => { + return ( + typeof config[key as keyof typeof config] === + typeof defaultEditorConfig[key as keyof typeof defaultEditorConfig] + ); + }); +}; + export const copyJSONText = async (data: T) => { return navigator.clipboard?.writeText(JSON.stringify(data)); }; From 5875270eafb853688891fada6668bd377ed7337f Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Thu, 9 Feb 2023 15:59:30 +0530 Subject: [PATCH 05/12] Add export validation logic and unit tests for the same --- __tests__/utils.test.tsx | 126 +++++++++++++++++- .../editor/SnippngConfigImportExporter.tsx | 103 ++++++++++---- components/editor/SnippngControlHeader.tsx | 47 +++---- components/form/Range.tsx | 11 +- lib/constants.ts | 19 +++ package.json | 4 +- types/editor-ti.ts | 57 ++++++++ types/index.ts | 4 + utils/index.ts | 78 +++++++++-- yarn.lock | 45 ++++++- 10 files changed, 422 insertions(+), 72 deletions(-) create mode 100644 types/editor-ti.ts diff --git a/__tests__/utils.test.tsx b/__tests__/utils.test.tsx index 6b9a8b3..197be42 100644 --- a/__tests__/utils.test.tsx +++ b/__tests__/utils.test.tsx @@ -1,4 +1,50 @@ -import { deepClone } from "@/utils"; +import { DEFAULT_RANGES, DEFAULT_WIDTHS } from "@/lib/constants"; +import { deepClone, validateSnippngConfig } from "@/utils"; + +const mockJSON = { + code: "", + snippetsName: "", + editorFontSize: 16, + editorWindowControlsType: "mac-left" as const, + fileName: "@utils/wfwfer.ts", + hasDropShadow: true, + lineHeight: 19, + paddingHorizontal: 70, + paddingVertical: 70, + rounded: true, + selectedLang: { label: "TypeScript", id: "typescript" }, + selectedTheme: { id: "tokyoNightStorm", label: "Tokyo Night Storm" }, + showFileName: true, + showLineNumbers: true, + wrapperBg: "#eee811", + gradients: ["#ba68c8", "#ffa7c4", "#e57373"], + gradientAngle: 140, + editorWidth: 450, + bgImageVisiblePatch: null, + bgBlur: 0, +}; + +const inValidJSON = { + invalidKey: "invalid value", +}; + +beforeAll(() => { + document.createRange = () => { + const range = new Range(); + + range.getBoundingClientRect = jest.fn(); + + range.getClientRects = () => { + return { + item: () => null, + length: 0, + [Symbol.iterator]: jest.fn(), + }; + }; + + return range; + }; +}); describe("Utils", () => { it("deep clones the javascript object", async () => { @@ -27,4 +73,82 @@ describe("Utils", () => { expect(clonedObject.marks.science).toBe(10); expect(clonedObject.birthDate).toBe(updatedDate); }); + + it("validates editor config coming from uploaded JSON file", async () => { + let minValues = DEFAULT_RANGES.min; + let maxValues = DEFAULT_RANGES.max; + const mockOneResult = validateSnippngConfig({ ...mockJSON }); + + const mockMissingKeyResult = validateSnippngConfig({ + ...mockJSON, + selectedLang: undefined as any, + }); + + const mockInvalidKeyResult = validateSnippngConfig({ + ...mockJSON, + selectedLang: [] as any, + }); + + const mockBlueCheckResult = validateSnippngConfig({ + ...mockJSON, + bgBlur: 100, + }); + const mockLineHeightResult = validateSnippngConfig({ + ...mockJSON, + lineHeight: 100, + }); + const mockPadHorResult = validateSnippngConfig({ + ...mockJSON, + paddingHorizontal: 200, + }); + const mockPadVerResult = validateSnippngConfig({ + ...mockJSON, + paddingVertical: 200, + }); + const mockFontSizeResult = validateSnippngConfig({ + ...mockJSON, + editorFontSize: 54, + }); + const mockGradAngResult = validateSnippngConfig({ + ...mockJSON, + gradientAngle: 361, + }); + const mockEdWidthMinResult = validateSnippngConfig({ + ...mockJSON, + editorWidth: -10, + }); + const mockEdWidthMaxResult = validateSnippngConfig({ + ...mockJSON, + editorWidth: 3000, + }); + const invalidResult = validateSnippngConfig({ ...(inValidJSON as any) }); + expect(mockOneResult).toBe(""); + expect(mockMissingKeyResult).toBe("value.selectedLang is missing"); + expect(mockInvalidKeyResult).toContain("missing"); + expect(mockBlueCheckResult).toBe( + `bgBlur value must be in the range ${minValues.BLUR} to ${maxValues.BLUR}` + ); + expect(mockLineHeightResult).toBe( + `lineHeight value must be in the range ${minValues.LINE_HEIGHT} to ${maxValues.LINE_HEIGHT}` + ); + expect(mockPadHorResult).toBe( + `paddingHorizontal value must be in the range ${minValues.PADDING_HORIZONTAL} to ${maxValues.PADDING_HORIZONTAL}` + ); + expect(mockPadVerResult).toBe( + `paddingVertical value must be in the range ${minValues.PADDING_VERTICAL} to ${maxValues.PADDING_VERTICAL}` + ); + expect(mockFontSizeResult).toBe( + `editorFontSize value must be in the range ${minValues.FONT_SIZE} to ${maxValues.FONT_SIZE}` + ); + expect(mockGradAngResult).toBe( + `gradientAngle value must be in the range ${minValues.GRADIENT_ANGLE} to ${maxValues.GRADIENT_ANGLE}` + ); + expect(mockEdWidthMinResult).toBe( + `editorWidth value must be in the range ${DEFAULT_WIDTHS.minWidth} to ${DEFAULT_WIDTHS.maxWidth}` + ); + expect(mockEdWidthMaxResult).toBe( + `editorWidth value must be in the range ${DEFAULT_WIDTHS.minWidth} to ${DEFAULT_WIDTHS.maxWidth}` + ); + expect(invalidResult).toBeTruthy(); + }); }); diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index be323ad..f1eb965 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -1,14 +1,16 @@ import { Dialog, Transition } from "@headlessui/react"; import { ArrowDownTrayIcon, + CheckCircleIcon, ClipboardDocumentIcon, CloudArrowDownIcon, XMarkIcon, } from "@heroicons/react/24/outline"; -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useState } from "react"; import { useSnippngEditor } from "@/context/SnippngEditorContext"; import { useToast } from "@/context/ToastContext"; +import { SnippngEditorConfigInterface } from "@/types"; import { clsx, copyJSONText, @@ -17,7 +19,6 @@ import { validateSnippngConfig, } from "@/utils"; import Button from "../form/Button"; -import { SnippngEditorConfigInterface } from "@/types"; interface Props { open: boolean; @@ -25,12 +26,13 @@ interface Props { } const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { - const { editorConfig } = useSnippngEditor(); + const { editorConfig, setEditorConfig } = useSnippngEditor(); + const { addToast } = useToast(); + const [isExport, setIsExport] = useState(true); + const [configError, setConfigError] = useState(""); const [configToBeImport, setConfigToBeImport] = useState(null); - const [isConfigValid, setIsConfigValid] = useState(false); - const { addToast } = useToast(); const getJsxByDatatype = (value: string | number | boolean) => { switch (typeof value) { @@ -96,9 +98,24 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { const file = e.target.files[0]; let reader = new FileReader(); reader.addEventListener("load", (e) => { - let jsonData = JSON.parse(e.target!.result! as string); - setConfigToBeImport(jsonData); - setIsConfigValid(validateSnippngConfig(jsonData)); + try { + let jsonData = JSON.parse(e.target!.result! as string); + let error = validateSnippngConfig(jsonData); + if (!error) setConfigToBeImport(jsonData); + else { + addToast({ + message: error, + type: "error", + }); + setConfigToBeImport(null); + } + setConfigError(error); + } catch (error) { + addToast({ + message: "Invalid json file", + type: "error", + }); + } }); reader.readAsText(file); e.target.value = ""; @@ -106,9 +123,16 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { reader.removeEventListener("load", () => {}); }; + const cleanUpOnClose = () => { + setConfigError(""); + setIsExport(true); + setConfigToBeImport(null); + onClose(); + }; + return ( - + = ({ open, onClose }) => { - {configToBeImport ? ( -
- {!isConfigValid ? ( - - Imported config is invalid - - ) : null} -
-
-                                        {"{"}
-                                        {getEditorConfigJsx({
-                                          ...deepClone(configToBeImport),
-                                        })}
-                                        {"}"}
-                                      
+
+ {configError ? ( + + Error: {configError} + + ) : null} + {configToBeImport ? ( +
+
+
+                                          {"{"}
+                                          {getEditorConfigJsx({
+                                            ...deepClone(configToBeImport),
+                                          })}
+                                          {"}"}
+                                        
+
+
-
- ) : null} + ) : null} +
)}
diff --git a/components/editor/SnippngControlHeader.tsx b/components/editor/SnippngControlHeader.tsx index 937a397..4cb5448 100644 --- a/components/editor/SnippngControlHeader.tsx +++ b/components/editor/SnippngControlHeader.tsx @@ -3,6 +3,7 @@ import { useToast } from "@/context/ToastContext"; import { ColorPicker } from "@/lib/color-picker"; import { defaultEditorConfig, + DEFAULT_RANGES, DOWNLOAD_OPTIONS, LANGUAGES, THEMES, @@ -213,7 +214,7 @@ const SnippngControlHeader: React.FC<{ leaveTo="transform opacity-0 scale-95" > - +
{ handleConfigChange("bgBlur")(+e.target.value); }} @@ -316,67 +316,62 @@ const SnippngControlHeader: React.FC<{
{ handleConfigChange("editorFontSize")(+e.target.value); }} - max={32} - min={10} - rangeMin={"10px"} - rangeMax={"32px"} + max={DEFAULT_RANGES.max.FONT_SIZE} + min={DEFAULT_RANGES.min.FONT_SIZE} />
{ handleConfigChange("paddingVertical")(+e.target.value); }} - max={100} - min={0} - rangeMin={"0px"} - rangeMax={"100px"} + max={DEFAULT_RANGES.max.PADDING_VERTICAL} + min={DEFAULT_RANGES.min.PADDING_VERTICAL} />
{ handleConfigChange("paddingHorizontal")(+e.target.value); }} - max={100} - min={0} - rangeMin={"0px"} - rangeMax={"100px"} + max={DEFAULT_RANGES.max.PADDING_HORIZONTAL} + min={DEFAULT_RANGES.min.PADDING_HORIZONTAL} />
{ handleConfigChange("lineHeight")(+e.target.value); }} - max={40} - min={10} - rangeMin={"10px"} - rangeMax={"40px"} + max={DEFAULT_RANGES.max.LINE_HEIGHT} + min={DEFAULT_RANGES.min.LINE_HEIGHT} />
{ handleConfigChange("gradientAngle")(+e.target.value); }} - max={360} - min={0} - rangeMin={"0deg"} - rangeMax={"360deg"} + max={DEFAULT_RANGES.max.GRADIENT_ANGLE} + min={DEFAULT_RANGES.min.GRADIENT_ANGLE} />
diff --git a/components/form/Range.tsx b/components/form/Range.tsx index ca2136f..0e2d053 100644 --- a/components/form/Range.tsx +++ b/components/form/Range.tsx @@ -2,11 +2,12 @@ import { clsx } from "@/utils"; import React from "react"; interface Props extends React.InputHTMLAttributes { - rangeMin: string; - rangeMax: string; + unit: string; label: string; + min: number; + max: number; } -const Range: React.FC = ({ rangeMax, rangeMin, label, ...props }) => { +const Range: React.FC = ({ unit = "px", label, ...props }) => { return (
@@ -22,8 +23,8 @@ const Range: React.FC = ({ rangeMax, rangeMin, label, ...props }) => { )} />
- {rangeMin} - {rangeMax} + {`${props.min}${unit}`} + {`${props.max}${unit}`}
diff --git a/lib/constants.ts b/lib/constants.ts index 0c17d82..ddb6be4 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -581,3 +581,22 @@ export const DEFAULT_WIDTHS = { minWidth: 320, maxWidth: 1200, }; + +export const DEFAULT_RANGES = { + min: { + BLUR: 0, + FONT_SIZE: 10, + PADDING_VERTICAL: 0, + PADDING_HORIZONTAL: 0, + LINE_HEIGHT: 10, + GRADIENT_ANGLE: 0, + }, + max: { + BLUR: 20, + FONT_SIZE: 32, + PADDING_VERTICAL: 100, + PADDING_HORIZONTAL: 100, + LINE_HEIGHT: 40, + GRADIENT_ANGLE: 360, + }, +}; diff --git a/package.json b/package.json index 86d5ca4..f633263 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-image-crop": "^10.0.9", + "ts-interface-checker": "^1.0.2", "typescript": "4.9.4" }, "devDependencies": { @@ -39,6 +40,7 @@ "jest-environment-jsdom": "^29.4.0", "next-router-mock": "^0.9.1", "postcss": "^8.4.21", - "tailwindcss": "^3.2.4" + "tailwindcss": "^3.2.4", + "ts-interface-builder": "^0.3.3" } } diff --git a/types/editor-ti.ts b/types/editor-ti.ts new file mode 100644 index 0000000..2f9bda3 --- /dev/null +++ b/types/editor-ti.ts @@ -0,0 +1,57 @@ +/** + * This module was automatically generated by `ts-interface-builder` + */ +import * as t from "ts-interface-checker"; +// tslint:disable:object-literal-key-quotes + +export const SelectOptionInterface = t.iface([], { + "id": "string", + "label": "string", +}); + +export const SnippngWindowControlsType = t.union(t.lit("mac-left"), t.lit("mac-right"), t.lit("windows-left"), t.lit("windows-right")); + +export const SnippngEditorConfigInterface = t.iface([], { + "ownerUid": t.opt("string"), + "uid": t.opt("string"), + "code": "string", + "editorFontSize": "number", + "editorWidth": "number", + "editorWindowControlsType": "SnippngWindowControlsType", + "gradients": t.array("string"), + "gradientAngle": "number", + "fileName": "string", + "hasDropShadow": "boolean", + "lineHeight": "number", + "paddingHorizontal": "number", + "paddingVertical": "number", + "rounded": "boolean", + "selectedLang": "SelectOptionInterface", + "selectedTheme": "SelectOptionInterface", + "showFileName": "boolean", + "showLineNumbers": "boolean", + "snippetsName": "string", + "wrapperBg": "string", + "bgImageVisiblePatch": t.union("string", "null"), + "bgBlur": "number", +}); + +export const CustomOmit = t.name("any"); + +export const SnippngExportableConfig = t.name("any"); + +export const SnippngEditorContextInterface = t.iface([], { + "editorConfig": "SnippngEditorConfigInterface", + "handleConfigChange": t.func(t.func("void", t.param("value", "V")), t.param("key", "K")), + "setEditorConfig": t.func("void", t.param("config", "SnippngEditorConfigInterface")), +}); + +const exportedTypeSuite: t.ITypeSuite = { + SelectOptionInterface, + SnippngWindowControlsType, + SnippngEditorConfigInterface, + CustomOmit, + SnippngExportableConfig, + SnippngEditorContextInterface, +}; +export default exportedTypeSuite; diff --git a/types/index.ts b/types/index.ts index 9c1e321..f6e4c58 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,3 +1,7 @@ +import exportedTypeSuite from "./editor-ti"; + export * from "./tsx"; export * from "./editor"; export * from "./toast"; + +export { exportedTypeSuite }; diff --git a/utils/index.ts b/utils/index.ts index aa60d2f..5672697 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,7 +1,12 @@ -import { SnippngEditorConfigInterface, SnippngExportableConfig } from "@/types"; +import { DEFAULT_RANGES, DEFAULT_WIDTHS } from "@/lib/constants"; +import { + exportedTypeSuite, + SnippngEditorConfigInterface, + SnippngExportableConfig, +} from "@/types"; import { langs } from "@uiw/codemirror-extensions-langs"; import * as themes from "@uiw/codemirror-themes-all"; -import { defaultEditorConfig } from "@/lib/constants"; +import { createCheckers } from "ts-interface-checker"; export const clsx = (...classNames: string[]) => classNames.filter(Boolean).join(" "); @@ -64,12 +69,69 @@ export const getExportableConfig = ( }; export const validateSnippngConfig = (config: SnippngEditorConfigInterface) => { - return Object.keys(config).every((key) => { - return ( - typeof config[key as keyof typeof config] === - typeof defaultEditorConfig[key as keyof typeof defaultEditorConfig] - ); - }); + const { SnippngEditorConfigInterface } = createCheckers(exportedTypeSuite); + try { + SnippngEditorConfigInterface.check(config); + let minValues = DEFAULT_RANGES.min; + let maxValues = DEFAULT_RANGES.max; + if (+config.bgBlur > maxValues.BLUR || +config.bgBlur < minValues.BLUR) { + throw Error( + `bgBlur value must be in the range ${minValues.BLUR} to ${maxValues.BLUR}` + ); + } + if ( + +config.lineHeight > maxValues.LINE_HEIGHT || + +config.lineHeight < minValues.LINE_HEIGHT + ) { + throw Error( + `lineHeight value must be in the range ${minValues.LINE_HEIGHT} to ${maxValues.LINE_HEIGHT}` + ); + } + if ( + +config.editorFontSize > maxValues.FONT_SIZE || + +config.editorFontSize < minValues.FONT_SIZE + ) { + throw Error( + `editorFontSize value must be in the range ${minValues.FONT_SIZE} to ${maxValues.FONT_SIZE}` + ); + } + if ( + +config.gradientAngle > maxValues.GRADIENT_ANGLE || + +config.gradientAngle < minValues.GRADIENT_ANGLE + ) { + throw Error( + `gradientAngle value must be in the range ${minValues.GRADIENT_ANGLE} to ${maxValues.GRADIENT_ANGLE}` + ); + } + if ( + +config.paddingHorizontal > maxValues.PADDING_HORIZONTAL || + +config.paddingHorizontal < minValues.PADDING_HORIZONTAL + ) { + throw Error( + `paddingHorizontal value must be in the range ${minValues.PADDING_HORIZONTAL} to ${maxValues.PADDING_HORIZONTAL}` + ); + } + if ( + +config.paddingVertical > maxValues.PADDING_VERTICAL || + +config.paddingVertical < minValues.PADDING_VERTICAL + ) { + throw Error( + `paddingVertical value must be in the range ${minValues.PADDING_VERTICAL} to ${maxValues.PADDING_VERTICAL}` + ); + } + if ( + +config.editorWidth && + (+config.editorWidth > DEFAULT_WIDTHS.maxWidth || +config.editorWidth < 0) + ) { + throw Error( + `editorWidth value must be in the range ${DEFAULT_WIDTHS.minWidth} to ${DEFAULT_WIDTHS.maxWidth}` + ); + } + + return ""; + } catch (error: any) { + return error?.message || "Invalid config"; + } }; export const copyJSONText = async (data: T) => { diff --git a/yarn.lock b/yarn.lock index 847ee5c..8c13657 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3394,7 +3394,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.20.0: +commander@^2.12.2, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4230,6 +4230,15 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +fs-extra@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -4425,7 +4434,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -5388,6 +5397,13 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -6850,6 +6866,21 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +ts-interface-builder@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/ts-interface-builder/-/ts-interface-builder-0.3.3.tgz#8b8f84677370a2660ae07fb556de59d06181a6c2" + integrity sha512-WHQwVBy0+Sv/jcHhKlyFgTyEVTM0GEPEw+gLmOYlZiJC1/eh5ah2EHSw7o+RUrl2grjEAMU6MTOItCuQIVJvnQ== + dependencies: + commander "^2.12.2" + fs-extra "^4.0.3" + glob "^7.1.6" + typescript "^3.0.0" + +ts-interface-checker@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-1.0.2.tgz#63f73a098b0ed34b982df1f490c54890e8e5e0b3" + integrity sha512-4IKKvhZRXhvtYF/mtu+OCfBqJKV6LczUq4kQYcpT+iSB7++R9+giWnp2ecwWMIcnG16btVOkXFnoxLSYMN1Q1g== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -6930,6 +6961,11 @@ typescript@4.9.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@^3.0.0: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -6970,6 +7006,11 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" From 808337b4a32d41b8ee6fe6abbab9939ef7ffc847 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Thu, 9 Feb 2023 19:59:36 +0530 Subject: [PATCH 06/12] Handle value for mapping context data on the screen --- components/editor/SnippngConfigImportExporter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index f1eb965..14417da 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -39,7 +39,7 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => { case "string": return ( - {`"${value}"` || <>""} + {value ? `"${value}"` : <>""} ); case "number": From c14f59620aff0d3fe1cb38f6e181203ac0caa068 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Fri, 10 Feb 2023 11:46:57 +0530 Subject: [PATCH 07/12] Add dome styles to tabs --- .../editor/SnippngConfigImportExporter.tsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index 14417da..a806f44 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -1,6 +1,7 @@ import { Dialog, Transition } from "@headlessui/react"; import { ArrowDownTrayIcon, + ArrowUpTrayIcon, CheckCircleIcon, ClipboardDocumentIcon, CloudArrowDownIcon, @@ -189,33 +190,40 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => {
-
+
+
From 033f7eed973d0597f73f769387469d95922055bc Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Fri, 10 Feb 2023 11:52:43 +0530 Subject: [PATCH 08/12] Add check for code in imported comfig --- components/editor/SnippngConfigImportExporter.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/editor/SnippngConfigImportExporter.tsx b/components/editor/SnippngConfigImportExporter.tsx index a806f44..e3fa574 100644 --- a/components/editor/SnippngConfigImportExporter.tsx +++ b/components/editor/SnippngConfigImportExporter.tsx @@ -347,9 +347,13 @@ const SnippngConfigImportExporter: React.FC = ({ open, onClose }) => {
From 152b4340e2c4b4e90b6d21259d5361118f159568 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Fri, 10 Feb 2023 16:02:25 +0530 Subject: [PATCH 10/12] Remove export keyword --- components/ThemeToggle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx index 2e3c0fc..642190a 100644 --- a/components/ThemeToggle.tsx +++ b/components/ThemeToggle.tsx @@ -1,6 +1,6 @@ import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; -export const ThemeToggle = () => { +const ThemeToggle = () => { const disableTransitionsTemporarily = () => { document.documentElement.classList.add("[&_*]:!transition-none"); window.setTimeout(() => { From a07afb17fa15526d93344d49ac01d002135d0106 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Fri, 10 Feb 2023 16:23:17 +0530 Subject: [PATCH 11/12] Add condition to only get analytics in production mode --- components/Toast.tsx | 7 +++---- config/firebase.ts | 6 ++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/Toast.tsx b/components/Toast.tsx index 7456936..7ff07a6 100644 --- a/components/Toast.tsx +++ b/components/Toast.tsx @@ -1,13 +1,12 @@ -import React, { useEffect } from "react"; -import { Fragment, useState } from "react"; +import { ToastInterface } from "@/types"; import { Transition } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon, InformationCircleIcon, XCircleIcon, } from "@heroicons/react/24/outline"; -import { XMarkIcon } from "@heroicons/react/20/solid"; -import { ToastInterface, ToastVariantType } from "@/types"; +import React, { Fragment, useEffect, useState } from "react"; interface Props extends ToastInterface { onClose: () => void; diff --git a/config/firebase.ts b/config/firebase.ts index 7065628..8bb9fc1 100644 --- a/config/firebase.ts +++ b/config/firebase.ts @@ -1,7 +1,7 @@ import { initializeApp } from "firebase/app"; import { Firestore, getFirestore } from "firebase/firestore"; import { Auth, getAuth } from "firebase/auth"; -import { Analytics, getAnalytics, isSupported } from "firebase/analytics"; +import { Analytics, getAnalytics } from "firebase/analytics"; let auth: Auth | undefined; let db: Firestore | undefined; @@ -22,7 +22,9 @@ try { auth = getAuth(app); db = getFirestore(app); - if (typeof window !== "undefined") analytics = getAnalytics(app); + if (process.env.NODE_ENV === "production" && typeof window !== "undefined") { + analytics = getAnalytics(app); + } } catch (error) { console.log( Error( From 9846bda2227644e1cea4e278f147022ef681ed77 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Fri, 10 Feb 2023 17:09:48 +0530 Subject: [PATCH 12/12] Change config fileName property in test --- __tests__/utils.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/utils.test.tsx b/__tests__/utils.test.tsx index 197be42..8c469ea 100644 --- a/__tests__/utils.test.tsx +++ b/__tests__/utils.test.tsx @@ -6,7 +6,7 @@ const mockJSON = { snippetsName: "", editorFontSize: 16, editorWindowControlsType: "mac-left" as const, - fileName: "@utils/wfwfer.ts", + fileName: "@utils/debounce.ts", hasDropShadow: true, lineHeight: 19, paddingHorizontal: 70,