Skip to content

Commit d1ea2d6

Browse files
committed
Add theme -> firebase and auth HOC
1 parent e8ef533 commit d1ea2d6

File tree

10 files changed

+360
-223
lines changed

10 files changed

+360
-223
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: 2 additions & 2 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

components/editor/SnippngControlHeader.tsx

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "@/lib/constants";
1212
import { ImagePicker } from "@/lib/image-picker";
1313
import { SelectOptionInterface } from "@/types";
14-
import { getEditorWrapperBg } from "@/utils";
14+
import { getEditorWrapperBg, loadThemes } from "@/utils";
1515
import { Menu, Transition } from "@headlessui/react";
1616
import {
1717
ArrowPathIcon,
@@ -27,23 +27,33 @@ import {
2727
} from "@heroicons/react/24/outline";
2828
import * as htmlToImage from "html-to-image";
2929
import { useRouter } from "next/router";
30-
import { Fragment, RefObject, useState } from "react";
30+
import { Fragment, RefObject, useEffect, useState } from "react";
3131
import Button from "../form/Button";
3232
import Checkbox from "../form/Checkbox";
3333
import Range from "../form/Range";
3434
import Select from "../form/Select";
3535
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+
}
3644

3745
const SnippngControlHeader: React.FC<{
3846
wrapperRef: RefObject<HTMLDivElement>;
3947
creatingTheme?: boolean;
4048
}> = ({ wrapperRef, creatingTheme }) => {
4149
const [openImportExportSidebar, setOpenImportExportSidebar] = useState(false);
50+
const [themes, setThemes] = useState<DropDownThemeItem[]>([]);
4251

4352
const { editorConfig, handleConfigChange, setEditorConfig } =
4453
useSnippngEditor();
4554

4655
const { addToast } = useToast();
56+
const { user } = useAuth();
4757
const router = useRouter();
4858

4959
const {
@@ -102,6 +112,30 @@ const SnippngControlHeader: React.FC<{
102112
});
103113
};
104114

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+
105139
return (
106140
<>
107141
<SnippngConfigImportExporter
@@ -130,16 +164,7 @@ const SnippngControlHeader: React.FC<{
130164
}
131165
handleConfigChange("selectedTheme")(val);
132166
}}
133-
options={getAvailableThemes()?.map((op) => {
134-
if (op?.isCustom) {
135-
// render icon for a custom theme
136-
return {
137-
...op,
138-
icon: FireIcon,
139-
};
140-
}
141-
return op;
142-
})}
167+
options={themes}
143168
/>
144169
) : null}
145170
<Select

components/editor/SnippngThemeBuilder.tsx

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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<{
@@ -15,9 +18,11 @@ const SnippngThemeBuilder: React.FC<{
1518
...themeConfig,
1619
isCustom: true,
1720
});
21+
const [saving, setSaving] = useState(false);
1822

1923
const { handleConfigChange, setEditorConfig } = useSnippngEditor();
2024
const { addToast } = useToast();
25+
const { user } = useAuth();
2126

2227
const onConfigChange = (key: keyof typeof theme.config, value: string) => {
2328
setTheme((prevTheme) => ({
@@ -29,30 +34,54 @@ const SnippngThemeBuilder: React.FC<{
2934
}));
3035
};
3136

32-
const saveAndApplyTheme = () => {
33-
let previousThemes =
34-
(LocalStorage.get("local_themes") as SnippngThemeAttributesInterface[]) ||
35-
[];
36-
let id = crypto.randomUUID();
37-
let themeToBeApplied = {
38-
id,
39-
label: theme.label,
40-
};
41-
previousThemes.push({
42-
...theme,
43-
id,
44-
});
45-
LocalStorage.set("local_themes", previousThemes);
46-
handleConfigChange("selectedTheme")(themeToBeApplied);
47-
addToast({
48-
message: "Theme saved successfully!",
49-
description: "You can view your custom themes in your profile",
50-
});
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+
}
5179
};
5280

5381
useEffect(() => {
5482
return () => {
55-
// 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
5685
let persistedEditorConfig = LocalStorage.get("config");
5786
setEditorConfig({
5887
...(persistedEditorConfig || defaultEditorConfig),
@@ -99,11 +128,12 @@ const SnippngThemeBuilder: React.FC<{
99128
})}
100129
</div>
101130
<Button
131+
disabled={saving}
102132
StartIcon={ArrowDownOnSquareIcon}
103133
className="w-full justify-center"
104134
onClick={saveAndApplyTheme}
105135
>
106-
Save theme
136+
{saving ? "Saving..." : "Save theme"}
107137
</Button>
108138
</div>
109139
<div className="w-full -mb-10 p-4">

components/form/Select.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { SelectOptionInterface } from "@/types";
22
import { clsx } from "@/utils";
33
import { Listbox, Transition } from "@headlessui/react";
44
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
5-
import { FireIcon } from "@heroicons/react/24/outline";
65
import React, { Fragment } from "react";
76

87
export interface SelectComponentProps {

components/profile/SnippngThemeItem.tsx

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { useToast } from "@/context/ToastContext";
22
import { DEFAULT_BASE_SETUP, DEFAULT_CODE_SNIPPET } from "@/lib/constants";
33
import { SnippngThemeAttributesInterface } from "@/types";
44
import { constructTheme, LocalStorage } from "@/utils";
5-
import { TrashIcon } from "@heroicons/react/24/outline";
5+
import { TrashIcon, EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
66
import { langs } from "@uiw/codemirror-extensions-langs";
77
import CodeMirror from "@uiw/react-codemirror";
88

9-
import React from "react";
9+
import React, { useState } from "react";
1010
import ErrorText from "../ErrorText";
11+
import { db } from "@/config/firebase";
12+
import { useAuth } from "@/context/AuthContext";
13+
import { deleteDoc, doc } from "firebase/firestore";
14+
import Loader from "../Loader";
1115

1216
interface Props {
1317
theme: SnippngThemeAttributesInterface;
@@ -16,6 +20,35 @@ interface Props {
1620

1721
const SnippngThemeItem: React.FC<Props> = ({ theme, onDelete }) => {
1822
const { addToast } = useToast();
23+
const { user } = useAuth();
24+
25+
const [deletingTheme, setDeletingTheme] = useState(false);
26+
27+
const deleteThemeItem = async (themeId: string) => {
28+
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.
29+
if (!user || !themeId) return;
30+
setDeletingTheme(true);
31+
32+
try {
33+
await deleteDoc(doc(db, "themes", themeId));
34+
let localThemes =
35+
(LocalStorage.get(
36+
"local_themes"
37+
) as SnippngThemeAttributesInterface[]) || [];
38+
localThemes = localThemes.filter((thm) => thm.id !== themeId);
39+
LocalStorage.set("local_themes", localThemes);
40+
onDelete(themeId || "");
41+
42+
addToast({
43+
message: "Theme deleted successfully",
44+
});
45+
} catch (error) {
46+
console.log("Error fetching snippets", error);
47+
} finally {
48+
setDeletingTheme(false);
49+
}
50+
};
51+
1952
if (!theme)
2053
return (
2154
<ErrorText
@@ -53,33 +86,26 @@ const SnippngThemeItem: React.FC<Props> = ({ theme, onDelete }) => {
5386
<button
5487
aria-label="delete-theme"
5588
title="Delete theme"
89+
disabled={deletingTheme}
5690
onClick={() => {
5791
let ok = confirm(
5892
"Are you sure you want to delete this theme permanently?"
5993
);
6094
if (!ok) return;
61-
let toBeDeletedId = theme.id;
62-
let localThemes =
63-
(LocalStorage.get(
64-
"local_themes"
65-
) as SnippngThemeAttributesInterface[]) || [];
66-
localThemes = localThemes.filter(
67-
(thm) => thm.id !== toBeDeletedId
68-
);
69-
LocalStorage.set("local_themes", localThemes);
70-
onDelete(toBeDeletedId);
71-
addToast({
72-
message: "Theme deleted successfully!",
73-
});
95+
deleteThemeItem(theme.id);
7496
}}
7597
className="inline-flex items-center gap-2 dark:text-white text-zinc-700"
7698
>
7799
{/* <PencilIcon className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer" /> */}
78-
<TrashIcon
79-
role={"button"}
80-
tabIndex={0}
81-
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
82-
/>
100+
{deletingTheme ? (
101+
<EllipsisHorizontalIcon className="animate-pulse h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer" />
102+
) : (
103+
<TrashIcon
104+
role={"button"}
105+
tabIndex={0}
106+
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
107+
/>
108+
)}
83109
</button>
84110
</span>
85111
</div>

context/AuthContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { auth } from "@/config/firebase";
1111
import { useToast } from "./ToastContext";
1212
import Layout from "@/layout/Layout";
1313
import { Loader } from "@/components";
14+
import { LocalStorage } from "@/utils";
1415

1516
const GithubProvider = new GithubAuthProvider();
1617
const GoogleProvider = new GoogleAuthProvider();
@@ -90,6 +91,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
9091
const logout = async () => {
9192
if (!auth) 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.
9293
await signOut(auth);
94+
LocalStorage.remove("local_themes");
9395
addToast({
9496
message: "Logged out successfully",
9597
description: "See you again!",

0 commit comments

Comments
 (0)