Skip to content

Commit 724d2c4

Browse files
authored
Merge pull request #36 from wajeshubham/dev
Dev
2 parents c0c445c + 31e298b commit 724d2c4

26 files changed

+923
-436
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ OR
173173
- [x] Import and export snippng config to quickly build the editor (download the JSON file with editor config)
174174
- [ ] Add theme presets to choose from
175175
- [ ] Option to create/save single default editor config for user
176-
- [ ] Custom theme configuration
176+
- [x] Custom theme configuration
177177
- [ ] Publish your themes
178178
- [ ] Build more themes to choose from
179179

__tests__/components/snippng_code_area.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { getEditorWrapperBg } from "@/utils";
55
import { render, screen, waitFor } from "@testing-library/react";
66
import { act } from "react-dom/test-utils";
77

8+
jest.mock("next/router", () => require("next-router-mock"));
9+
810
beforeEach(() => {
911
// IntersectionObserver isn't available in test environment
1012
const mockIntersectionObserver = jest.fn();

__tests__/components/snippng_control_header.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { defaultEditorConfig, LANGUAGES, THEMES } from "@/lib/constants";
44
import { render, screen, waitFor } from "@testing-library/react";
55
import { act } from "react-dom/test-utils";
66

7+
jest.mock("next/router", () => require("next-router-mock"));
8+
79
beforeAll(() => {
810
document.createRange = () => {
911
const range = new Range();

components/editor/SnippngCodeArea.tsx

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useEffect, useRef, useState } from "react";
22

3-
import { DEFAULT_BASE_SETUP } from "@/lib/constants";
3+
import { DEFAULT_BASE_SETUP, THEMES } from "@/lib/constants";
44
import {
55
clsx,
6+
constructTheme,
67
deepClone,
78
getEditorWrapperBg,
89
getLanguage,
@@ -25,6 +26,7 @@ import SnippngWindowControls from "./SnippngWindowControls";
2526
import { db } from "@/config/firebase";
2627
import { useAuth } from "@/context/AuthContext";
2728
import { useToast } from "@/context/ToastContext";
29+
import { SnippngThemeAttributesInterface } from "@/types";
2830
import {
2931
ArrowDownOnSquareStackIcon,
3032
ArrowPathIcon,
@@ -33,7 +35,11 @@ import {
3335
import { addDoc, collection, doc, updateDoc } from "firebase/firestore";
3436
import Logo from "../Logo";
3537

36-
const SnippngCodeArea = () => {
38+
interface Props {
39+
underConstructionTheme?: SnippngThemeAttributesInterface;
40+
}
41+
42+
const SnippngCodeArea: React.FC<Props> = ({ underConstructionTheme }) => {
3743
const editorRef = useRef<HTMLDivElement>(null); // useRef to persist existing ref. Might be useful when dealing with background image in future
3844
const wrapperRef = useRef<HTMLDivElement>(null);
3945
const [saving, setSaving] = useState(false);
@@ -110,16 +116,40 @@ const SnippngCodeArea = () => {
110116
}
111117
};
112118

119+
/**
120+
*
121+
* @returns editor compatible theme object
122+
* @description Function is responsible for constructing theme based on if it is a
123+
* - `predefined` - theme in the `@uiw/codemirror-themes-all` library
124+
* - `localCustom` - Build by user and saved locally
125+
* - `underConstructionTheme` - user is currently constructing/configuring new theme
126+
*/
127+
const getSelectedTheme = () => {
128+
// If user is configuring the custom theme (SnippngCustomThemeContextProvider modal is mounded)
129+
if (underConstructionTheme) return constructTheme(underConstructionTheme);
130+
else {
131+
// check if selected theme is predefined or locally created
132+
let [isPredefined, isLocalCustom] = getTheme(selectedTheme.id);
133+
134+
if (isPredefined) return themes[isPredefined]; // if it is predefined return the theme configuration from the library
135+
136+
if (isLocalCustom) return constructTheme(isLocalCustom); // else construct and return the code mirror compatible theme
137+
138+
return themes[THEMES[0].id as keyof typeof themes]; // this will be returned if theme is not predefined as well as not available locally
139+
}
140+
};
141+
113142
useEffect(() => {
114143
// if there is a uid means we are on edit page where we want to avoid persisting the editor config
115-
if (uid) return;
144+
// underConstructionTheme means user is on the theme page which we don't want to persist
145+
if (uid || underConstructionTheme) return;
116146
// persist the editor config changes only when user is creating new snippet
117147
LocalStorage.set("config", {
118148
...editorConfig,
119149
uid: undefined,
120150
ownerUid: undefined,
121151
});
122-
}, [editorConfig, uid]);
152+
}, [editorConfig, uid, underConstructionTheme]);
123153

124154
return (
125155
<>
@@ -131,7 +161,10 @@ const SnippngCodeArea = () => {
131161
<NoSSRWrapper>
132162
<div className="rounded-md bg-white dark:bg-zinc-900 md:p-8 p-4 flex justify-center border-[1px] flex-col items-center dark:border-zinc-500 border-zinc-200 shadow-md w-full">
133163
<div className="w-full">
134-
<SnippngControlHeader wrapperRef={wrapperRef} />
164+
<SnippngControlHeader
165+
creatingTheme={!!underConstructionTheme}
166+
wrapperRef={wrapperRef}
167+
/>
135168
</div>
136169
{bgImageVisiblePatch ? (
137170
<button
@@ -200,7 +233,7 @@ const SnippngCodeArea = () => {
200233
fontSize: `${editorFontSize}px`,
201234
}}
202235
// @ts-ignore
203-
theme={themes[getTheme(selectedTheme.id)]}
236+
theme={getSelectedTheme()}
204237
indentWithTab
205238
onChange={(value) => handleConfigChange("code")(value)}
206239
>
@@ -232,64 +265,65 @@ const SnippngCodeArea = () => {
232265
) : null}
233266
</div>
234267
</div>
235-
<div className="w-full mt-8 flex md:flex-row flex-col gap-4 justify-start items-center">
236-
<div className="w-full">
237-
<Input
238-
value={snippetsName}
239-
onChange={(e) =>
240-
handleConfigChange("snippetsName")(e.target.value)
241-
}
242-
placeholder="Snippet name..."
243-
/>
244-
</div>
245-
<div className="flex flex-shrink-0 gap-4 md:flex-row flex-col md:w-fit w-full">
246-
<Button
247-
id="save-snippet-btn"
248-
StartIcon={ArrowDownOnSquareStackIcon}
249-
disabled={saving}
250-
onClick={(e) => {
251-
e.stopPropagation();
252-
if (!user)
253-
return addToast({
254-
message: "Please login first",
255-
type: "error",
256-
description:
257-
"You need to login before saving the snippet",
258-
});
259-
if (!snippetsName)
260-
return addToast({
261-
message: "Snippet name is required",
262-
type: "error",
263-
});
264-
else saveSnippet();
265-
}}
266-
>
267-
{saving
268-
? "Saving..."
269-
: uid // if there is a uid, we are on snippet details page where user can copy the snippet
270-
? "Fork snippet"
271-
: "Save snippet"}
272-
</Button>
273-
{uid && user && user.uid === ownerUid ? (
268+
{!underConstructionTheme ? (
269+
<div className="w-full mt-8 flex md:flex-row flex-col gap-4 justify-start items-center">
270+
<div className="w-full">
271+
<Input
272+
value={snippetsName}
273+
onChange={(e) =>
274+
handleConfigChange("snippetsName")(e.target.value)
275+
}
276+
placeholder="Snippet name..."
277+
/>
278+
</div>
279+
<div className="flex flex-shrink-0 gap-4 md:flex-row flex-col md:w-fit w-full">
274280
<Button
275-
StartIcon={ArrowPathIcon}
276-
disabled={updating}
281+
id="save-snippet-btn"
282+
StartIcon={ArrowDownOnSquareStackIcon}
283+
disabled={saving}
277284
onClick={(e) => {
278285
e.stopPropagation();
286+
if (!user)
287+
return addToast({
288+
message: "Please login first",
289+
type: "error",
290+
description:
291+
"You need to login before saving the snippet",
292+
});
279293
if (!snippetsName)
280294
return addToast({
281295
message: "Snippet name is required",
282296
type: "error",
283297
});
284-
updateSnippet();
298+
else saveSnippet();
285299
}}
286300
>
287-
{updating ? "Updating..." : "Update snippet"}
301+
{saving
302+
? "Saving..."
303+
: uid // if there is a uid, we are on snippet details page where user can copy the snippet
304+
? "Fork snippet"
305+
: "Save snippet"}
288306
</Button>
289-
) : null}
307+
{uid && user && user.uid === ownerUid ? (
308+
<Button
309+
StartIcon={ArrowPathIcon}
310+
disabled={updating}
311+
onClick={(e) => {
312+
e.stopPropagation();
313+
if (!snippetsName)
314+
return addToast({
315+
message: "Snippet name is required",
316+
type: "error",
317+
});
318+
updateSnippet();
319+
}}
320+
>
321+
{updating ? "Updating..." : "Update snippet"}
322+
</Button>
323+
) : null}
324+
</div>
290325
</div>
291-
</div>
292-
{/* TODO: Add CTA to remove background image */}
326+
) : null}
293327
</div>
294328
{uid ? (
295329
<small className="dark:text-zinc-300 text-left text-zinc-600 py-2 inline-block">

components/editor/SnippngControlHeader.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import {
55
defaultEditorConfig,
66
DEFAULT_RANGES,
77
DOWNLOAD_OPTIONS,
8+
getAvailableThemes,
89
LANGUAGES,
9-
THEMES,
10+
predefinedConfig,
1011
} from "@/lib/constants";
1112
import { ImagePicker } from "@/lib/image-picker";
1213
import { SelectOptionInterface } from "@/types";
13-
import { getEditorWrapperBg } from "@/utils";
14+
import { getEditorWrapperBg, LocalStorage } from "@/utils";
1415
import { Menu, Transition } from "@headlessui/react";
1516
import {
17+
ArrowPathIcon,
1618
ArrowsUpDownIcon,
1719
ChevronDownIcon,
1820
CloudArrowDownIcon,
@@ -23,6 +25,7 @@ import {
2325
SparklesIcon,
2426
} from "@heroicons/react/24/outline";
2527
import * as htmlToImage from "html-to-image";
28+
import { useRouter } from "next/router";
2629
import { Fragment, RefObject, useState } from "react";
2730
import Button from "../form/Button";
2831
import Checkbox from "../form/Checkbox";
@@ -32,11 +35,15 @@ import SnippngConfigImportExporter from "./SnippngConfigImportExporter";
3235

3336
const SnippngControlHeader: React.FC<{
3437
wrapperRef: RefObject<HTMLDivElement>;
35-
}> = ({ wrapperRef }) => {
38+
creatingTheme?: boolean;
39+
}> = ({ wrapperRef, creatingTheme }) => {
3640
const [openImportExportSidebar, setOpenImportExportSidebar] = useState(false);
3741

38-
const { editorConfig, handleConfigChange } = useSnippngEditor();
42+
const { editorConfig, handleConfigChange, setEditorConfig } =
43+
useSnippngEditor();
44+
3945
const { addToast } = useToast();
46+
const router = useRouter();
4047

4148
const {
4249
code,
@@ -96,7 +103,6 @@ const SnippngControlHeader: React.FC<{
96103

97104
return (
98105
<>
99-
{/* headless ui renders this sidebar in portal */}
100106
<SnippngConfigImportExporter
101107
open={openImportExportSidebar}
102108
onClose={() => {
@@ -111,15 +117,21 @@ const SnippngControlHeader: React.FC<{
111117
options={[...DOWNLOAD_OPTIONS]}
112118
/>
113119

114-
<Select
115-
Icon={SparklesIcon}
116-
value={selectedTheme}
117-
onChange={(val) => {
118-
if (!val.id) return;
119-
handleConfigChange("selectedTheme")(val);
120-
}}
121-
options={[...THEMES]}
122-
/>
120+
{!creatingTheme ? (
121+
<Select
122+
Icon={SparklesIcon}
123+
value={selectedTheme}
124+
onChange={(val) => {
125+
if (!val.id) return;
126+
if (val.id === "create_new") {
127+
router.push("/theme/create");
128+
return;
129+
}
130+
handleConfigChange("selectedTheme")(val);
131+
}}
132+
options={getAvailableThemes()}
133+
/>
134+
) : null}
123135
<Select
124136
Icon={CommandLineIcon}
125137
value={selectedLang}
@@ -224,6 +236,14 @@ const SnippngControlHeader: React.FC<{
224236
<ArrowsUpDownIcon className="h-5 w-5 mr-2" /> Import or export
225237
config
226238
</Menu.Button>
239+
<Menu.Button
240+
className="w-full text-left p-2 inline-flex items-center"
241+
onClick={() => {
242+
setEditorConfig({ ...predefinedConfig });
243+
}}
244+
>
245+
<ArrowPathIcon className="h-5 w-5 mr-2" /> Reset settings
246+
</Menu.Button>
227247
<div className="py-1 px-2">
228248
<Checkbox
229249
label="Watermark"

0 commit comments

Comments
 (0)