Skip to content

Commit fe1db8f

Browse files
authored
Merge pull request #39 from wajeshubham/dev
Dev
2 parents d00b544 + d1ea2d6 commit fe1db8f

File tree

12 files changed

+385
-219
lines changed

12 files changed

+385
-219
lines changed

HOC/withAuth.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 = <T extends Object>(Component: ComponentType<T>) => {
11+
const InnerComponent = (props: T) => {
12+
const { user } = useAuth();
13+
14+
if (!user)
15+
return (
16+
<Layout title={`Snippng | code snippets to image`}>
17+
<div
18+
data-testid="signin-btn-container"
19+
className="w-full h-full flex justify-center items-center py-32"
20+
>
21+
<SigninButton />
22+
</div>
23+
</Layout>
24+
);
25+
26+
return <Component {...props} />;
27+
};
28+
return InnerComponent;
29+
};
30+
31+
export default withAuth;

components/editor/SnippngCodeArea.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "react";
22

33
import { DEFAULT_BASE_SETUP, THEMES } from "@/lib/constants";
44
import {
5+
LocalStorage,
56
clsx,
67
constructTheme,
78
deepClone,
89
getEditorWrapperBg,
910
getLanguage,
1011
getTheme,
11-
LocalStorage,
1212
} from "@/utils";
1313

1414
import { langs, loadLanguage } from "@uiw/codemirror-extensions-langs";
@@ -17,9 +17,9 @@ import CodeMirror from "@uiw/react-codemirror";
1717

1818
import { useSnippngEditor } from "@/context/SnippngEditorContext";
1919
import { WidthHandler } from "@/lib/width-handler";
20+
import NoSSRWrapper from "../NoSSRWrapper";
2021
import Button from "../form/Button";
2122
import Input from "../form/Input";
22-
import NoSSRWrapper from "../NoSSRWrapper";
2323
import SnippngControlHeader from "./SnippngControlHeader";
2424
import SnippngWindowControls from "./SnippngWindowControls";
2525

@@ -119,7 +119,7 @@ const SnippngCodeArea: React.FC<Props> = ({ underConstructionTheme }) => {
119119
/**
120120
*
121121
* @returns editor compatible theme object
122-
* @description Function is responsible for constructing theme based on if it is a
122+
* @description Function is responsible for constructing theme based on if it is
123123
* - `predefined` - theme in the `@uiw/codemirror-themes-all` library
124124
* - `localCustom` - Build by user and saved locally
125125
* - `underConstructionTheme` - user is currently constructing/configuring new theme

components/editor/SnippngControlHeader.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { useSnippngEditor } from "@/context/SnippngEditorContext";
22
import { useToast } from "@/context/ToastContext";
33
import { ColorPicker } from "@/lib/color-picker";
44
import {
5-
defaultEditorConfig,
65
DEFAULT_RANGES,
76
DOWNLOAD_OPTIONS,
8-
getAvailableThemes,
97
LANGUAGES,
8+
defaultEditorConfig,
9+
getAvailableThemes,
1010
predefinedConfig,
1111
} from "@/lib/constants";
1212
import { ImagePicker } from "@/lib/image-picker";
1313
import { SelectOptionInterface } from "@/types";
14-
import { getEditorWrapperBg, LocalStorage } from "@/utils";
14+
import { getEditorWrapperBg, loadThemes } from "@/utils";
1515
import { Menu, Transition } from "@headlessui/react";
1616
import {
1717
ArrowPathIcon,
@@ -21,28 +21,39 @@ import {
2121
Cog6ToothIcon,
2222
CommandLineIcon,
2323
DocumentDuplicateIcon,
24+
FireIcon,
2425
PhotoIcon,
2526
SparklesIcon,
2627
} from "@heroicons/react/24/outline";
2728
import * as htmlToImage from "html-to-image";
2829
import { useRouter } from "next/router";
29-
import { Fragment, RefObject, useState } from "react";
30+
import { Fragment, RefObject, useEffect, useState } from "react";
3031
import Button from "../form/Button";
3132
import Checkbox from "../form/Checkbox";
3233
import Range from "../form/Range";
3334
import Select from "../form/Select";
3435
import SnippngConfigImportExporter from "./SnippngConfigImportExporter";
36+
import { useAuth } from "@/context/AuthContext";
37+
38+
interface DropDownThemeItem {
39+
icon?: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
40+
id: string;
41+
label: string;
42+
isCustom?: boolean | undefined;
43+
}
3544

3645
const SnippngControlHeader: React.FC<{
3746
wrapperRef: RefObject<HTMLDivElement>;
3847
creatingTheme?: boolean;
3948
}> = ({ wrapperRef, creatingTheme }) => {
4049
const [openImportExportSidebar, setOpenImportExportSidebar] = useState(false);
50+
const [themes, setThemes] = useState<DropDownThemeItem[]>([]);
4151

4252
const { editorConfig, handleConfigChange, setEditorConfig } =
4353
useSnippngEditor();
4454

4555
const { addToast } = useToast();
56+
const { user } = useAuth();
4657
const router = useRouter();
4758

4859
const {
@@ -101,6 +112,30 @@ const SnippngControlHeader: React.FC<{
101112
});
102113
};
103114

115+
const setThemesForDropDown = () => {
116+
const dropDownThemes = getAvailableThemes()?.map((op) => {
117+
if (op?.isCustom) {
118+
// render icon for a custom theme
119+
return {
120+
...op,
121+
icon: FireIcon,
122+
};
123+
}
124+
return { ...op, icon: undefined };
125+
});
126+
setThemes(dropDownThemes);
127+
};
128+
129+
useEffect(() => {
130+
if (!user) {
131+
setThemesForDropDown();
132+
} else {
133+
loadThemes(user, (themes) => {
134+
setThemesForDropDown();
135+
});
136+
}
137+
}, [user]);
138+
104139
return (
105140
<>
106141
<SnippngConfigImportExporter
@@ -129,7 +164,7 @@ const SnippngControlHeader: React.FC<{
129164
}
130165
handleConfigChange("selectedTheme")(val);
131166
}}
132-
options={getAvailableThemes()}
167+
options={themes}
133168
/>
134169
) : null}
135170
<Select

components/editor/SnippngThemeBuilder.tsx

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import { Button, Input, SnippngCodeArea } from "@/components";
2+
import { db } from "@/config/firebase";
3+
import { useAuth } from "@/context/AuthContext";
24
import { useSnippngEditor } from "@/context/SnippngEditorContext";
35
import { useToast } from "@/context/ToastContext";
46
import { ColorPicker } from "@/lib/color-picker";
57
import { defaultCustomTheme, defaultEditorConfig } from "@/lib/constants";
68
import { SnippngThemeAttributesInterface } from "@/types";
7-
import { LocalStorage } from "@/utils";
9+
import { LocalStorage, deepClone } from "@/utils";
810
import { ArrowDownOnSquareIcon } from "@heroicons/react/24/outline";
11+
import { addDoc, collection } from "firebase/firestore";
912
import React, { useEffect, useState } from "react";
1013

1114
const SnippngThemeBuilder: React.FC<{
1215
themeConfig?: SnippngThemeAttributesInterface;
1316
}> = ({ themeConfig = { ...defaultCustomTheme } }) => {
1417
const [theme, setTheme] = useState<SnippngThemeAttributesInterface>({
1518
...themeConfig,
19+
isCustom: true,
1620
});
21+
const [saving, setSaving] = useState(false);
1722

1823
const { handleConfigChange, setEditorConfig } = useSnippngEditor();
1924
const { addToast } = useToast();
25+
const { user } = useAuth();
2026

2127
const onConfigChange = (key: keyof typeof theme.config, value: string) => {
2228
setTheme((prevTheme) => ({
@@ -28,30 +34,54 @@ const SnippngThemeBuilder: React.FC<{
2834
}));
2935
};
3036

31-
const saveAndApplyTheme = () => {
32-
let previousThemes =
33-
(LocalStorage.get("local_themes") as SnippngThemeAttributesInterface[]) ||
34-
[];
35-
let id = crypto.randomUUID();
36-
let themeToBeApplied = {
37-
id,
38-
label: theme.label,
39-
};
40-
previousThemes.push({
41-
...theme,
42-
id,
43-
});
44-
LocalStorage.set("local_themes", previousThemes);
45-
handleConfigChange("selectedTheme")(themeToBeApplied);
46-
addToast({
47-
message: "Theme saved successfully!",
48-
description: "You can view your custom themes in your profile",
49-
});
37+
const saveAndApplyTheme = async () => {
38+
if (!db) return console.log(Error("Firebase is not configured")); // This is to handle error when there is no `.env` file. So, that app doesn't crash while developing without `.env` file.
39+
if (!user) return;
40+
setSaving(true);
41+
try {
42+
const dataToBeAdded = {
43+
...deepClone(theme), // deep clone the theme to avoid mutation
44+
ownerUid: user.uid,
45+
owner: {
46+
displayName: user?.displayName,
47+
email: user?.email,
48+
photoURL: user?.photoURL,
49+
},
50+
};
51+
const savedDoc = await addDoc(collection(db, "themes"), {
52+
...dataToBeAdded,
53+
});
54+
if (savedDoc.id) {
55+
// get previously saved themes
56+
let previousThemes =
57+
(LocalStorage.get(
58+
"local_themes"
59+
) as SnippngThemeAttributesInterface[]) || [];
60+
61+
// push newly created theme inside the previous themes array
62+
previousThemes.push({
63+
...dataToBeAdded,
64+
id: savedDoc.id,
65+
});
66+
// store the newly created theme inside local storage
67+
LocalStorage.set("local_themes", previousThemes);
68+
69+
addToast({
70+
message: "Theme saved successfully!",
71+
description: "You can view your custom themes in your profile",
72+
});
73+
}
74+
} catch (e) {
75+
console.error("Error adding document: ", e);
76+
} finally {
77+
setSaving(false);
78+
}
5079
};
5180

5281
useEffect(() => {
5382
return () => {
54-
// to avoid changing main editor's config state while creating theme we will set the persisted editor state
83+
// to avoid changing main editor's config state while creating theme
84+
// we will set the persisted editor state
5585
let persistedEditorConfig = LocalStorage.get("config");
5686
setEditorConfig({
5787
...(persistedEditorConfig || defaultEditorConfig),
@@ -98,11 +128,12 @@ const SnippngThemeBuilder: React.FC<{
98128
})}
99129
</div>
100130
<Button
131+
disabled={saving}
101132
StartIcon={ArrowDownOnSquareIcon}
102133
className="w-full justify-center"
103134
onClick={saveAndApplyTheme}
104135
>
105-
Save theme
136+
{saving ? "Saving..." : "Save theme"}
106137
</Button>
107138
</div>
108139
<div className="w-full -mb-10 p-4">

components/form/Select.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import React, { Fragment } from "react";
66

77
export interface SelectComponentProps {
88
value: SelectOptionInterface;
9-
options: { id: string; label: string }[];
9+
options: {
10+
id: string;
11+
label: string;
12+
icon?: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
13+
}[];
1014
onChange: (value: SelectOptionInterface) => void;
1115
children?: React.ReactNode;
1216
placeholder?: string | React.ReactNode;
@@ -73,9 +77,15 @@ const Select: React.FC<SelectComponentProps> = ({
7377
value.id === option.id
7478
? "font-semibold"
7579
: "font-normal",
76-
"block truncate"
80+
"inline-flex truncate justify-start items-center gap-2"
7781
)}
7882
>
83+
{option.icon ? (
84+
<option.icon
85+
className="h-5 w-5"
86+
aria-hidden="true"
87+
/>
88+
) : null}
7989
{option.label}
8090
</span>
8191

0 commit comments

Comments
 (0)