From d41af26e95496fd02a989c229a894d1325b0ca6d Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Sun, 28 May 2023 03:14:41 +0530 Subject: [PATCH 1/6] Add publish status toggle in theme card in the user profile --- components/editor/SnippngThemeBuilder.tsx | 1 + components/profile/SnippngThemeItem.tsx | 87 +++++++++++++++++++++-- pages/profile.tsx | 11 +++ types/editor.ts | 2 +- 4 files changed, 93 insertions(+), 8 deletions(-) diff --git a/components/editor/SnippngThemeBuilder.tsx b/components/editor/SnippngThemeBuilder.tsx index 3a84905..d0506af 100644 --- a/components/editor/SnippngThemeBuilder.tsx +++ b/components/editor/SnippngThemeBuilder.tsx @@ -42,6 +42,7 @@ const SnippngThemeBuilder: React.FC<{ const dataToBeAdded = { ...deepClone(theme), // deep clone the theme to avoid mutation ownerUid: user.uid, + isPublished: false, owner: { displayName: user?.displayName, email: user?.email, diff --git a/components/profile/SnippngThemeItem.tsx b/components/profile/SnippngThemeItem.tsx index a8fc3cc..780e940 100644 --- a/components/profile/SnippngThemeItem.tsx +++ b/components/profile/SnippngThemeItem.tsx @@ -2,7 +2,12 @@ import { useToast } from "@/context/ToastContext"; import { DEFAULT_BASE_SETUP, DEFAULT_CODE_SNIPPET } from "@/lib/constants"; import { SnippngThemeAttributesInterface } from "@/types"; import { constructTheme, LocalStorage } from "@/utils"; -import { TrashIcon, EllipsisHorizontalIcon } from "@heroicons/react/24/outline"; +import { + TrashIcon, + EllipsisHorizontalIcon, + EyeIcon, + EyeSlashIcon, +} from "@heroicons/react/24/outline"; import { langs } from "@uiw/codemirror-extensions-langs"; import CodeMirror from "@uiw/react-codemirror"; @@ -10,15 +15,21 @@ import React, { useState } from "react"; import ErrorText from "../ErrorText"; import { db } from "@/config/firebase"; import { useAuth } from "@/context/AuthContext"; -import { deleteDoc, doc } from "firebase/firestore"; +import { deleteDoc, doc, updateDoc } from "firebase/firestore"; import Loader from "../Loader"; +import Button from "../form/Button"; interface Props { theme: SnippngThemeAttributesInterface; onDelete: (themeId: string) => void; + onPublishChange: (themeId: string) => void; } -const SnippngThemeItem: React.FC = ({ theme, onDelete }) => { +const SnippngThemeItem: React.FC = ({ + theme, + onDelete, + onPublishChange, +}) => { const { addToast } = useToast(); const { user } = useAuth(); @@ -43,12 +54,44 @@ const SnippngThemeItem: React.FC = ({ theme, onDelete }) => { message: "Theme deleted successfully", }); } catch (error) { - console.log("Error fetching snippets", error); + console.log("Error deleting theme", error); } finally { setDeletingTheme(false); } }; + const togglePublishThemeItem = async (themeId: string) => { + 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. + if (!user || !themeId) return; + + try { + await updateDoc(doc(db, "themes", themeId), { + ...theme, + isPublished: !theme.isPublished, + }); + + let localThemes = + (LocalStorage.get( + "local_themes" + ) as SnippngThemeAttributesInterface[]) || []; + localThemes = localThemes.map((thm) => { + if (thm.id === themeId) { + thm.isPublished = !thm.isPublished; + } + return thm; + }); + LocalStorage.set("local_themes", localThemes); + addToast({ + message: `Theme ${ + theme.isPublished ? "unpublished" : "published" + } successfully`, + }); + onPublishChange(themeId || ""); + } catch (error) { + console.log("Error publishing theme", error); + } + }; + if (!theme) return ( = ({ theme, onDelete }) => { theme={constructTheme(theme)} indentWithTab > - {theme.isPublic ? ( - + {theme.isPublished ? ( + Published ) : null} @@ -83,6 +126,36 @@ const SnippngThemeItem: React.FC = ({ theme, onDelete }) => { {theme.label} + + {theme?.ownerUid === user?.uid ? ( + + ) : null} + + + + +

+ {theme?.owner?.displayName || "Snippng user"} +

+

+ {theme?.owner?.email || "Snippng user"} +

+
+
); diff --git a/pages/explore/themes/index.tsx b/pages/explore/themes/index.tsx new file mode 100644 index 0000000..d202af3 --- /dev/null +++ b/pages/explore/themes/index.tsx @@ -0,0 +1,27 @@ +import PublishedThemeListing from "@/components/explore/PublishedThemeListing"; +import Layout from "@/layout/Layout"; +import { SparklesIcon } from "@heroicons/react/24/outline"; +import React from "react"; + +const PublishedThemes = () => { + return ( + +
+

+ Explore themes +

+
+

+ Explore and fork themes configured by other developers in community +

+
+
+ +
+ ); +}; + +export default PublishedThemes; diff --git a/styles/globals.css b/styles/globals.css index 2107df0..e15cbad 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -37,7 +37,7 @@ body { } .CodeMirror__Theme__Preview__Editor div.cm-editor { - @apply pt-3 mb-10 border-[0.1px] dark:border-zinc-400 border-zinc-300 rounded-md; + @apply pt-3 mb-[6.5rem] border-[0.1px] dark:border-zinc-400 border-zinc-300 rounded-md; } .cm-scroller { diff --git a/types/editor.ts b/types/editor.ts index 8e9b07c..d5e2b2b 100644 --- a/types/editor.ts +++ b/types/editor.ts @@ -1,4 +1,5 @@ import { createTheme } from "@uiw/codemirror-themes"; +import { User } from "firebase/auth"; export interface SelectOptionInterface { id: string; @@ -11,6 +12,8 @@ export interface SnippngThemeAttributesInterface { label: string; theme: "light" | "dark"; isPublished?: boolean; + owner?: Pick; + ownerUid?: string; config: { background: string; foreground: string; From e4fb51bb6f6cef43243696f7638521a2019dc725 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Sun, 28 May 2023 03:48:07 +0530 Subject: [PATCH 3/6] Fix loader screen z index issue --- components/Loader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Loader.tsx b/components/Loader.tsx index defef59..b48d6b1 100644 --- a/components/Loader.tsx +++ b/components/Loader.tsx @@ -2,7 +2,7 @@ import React from "react"; const Loader = () => { return ( -
+
Date: Sun, 28 May 2023 20:54:21 +0530 Subject: [PATCH 4/6] Add explore theme nav link with clone theme functionality --- components/explore/PublishedThemeListing.tsx | 18 +- components/profile/SnippngThemeItem.tsx | 186 +++++++++++++------ layout/Header.tsx | 24 ++- types/editor.ts | 1 + 4 files changed, 160 insertions(+), 69 deletions(-) diff --git a/components/explore/PublishedThemeListing.tsx b/components/explore/PublishedThemeListing.tsx index 40ad85b..db6ed04 100644 --- a/components/explore/PublishedThemeListing.tsx +++ b/components/explore/PublishedThemeListing.tsx @@ -1,18 +1,19 @@ import { db } from "@/config/firebase"; import { SnippngThemeAttributesInterface } from "@/types"; -import { collection, getDocs, query, where } from "firebase/firestore"; -import React, { useEffect, useState } from "react"; -import ErrorText from "../ErrorText"; import { SparklesIcon } from "@heroicons/react/24/outline"; -import SnippngThemeItem from "../profile/SnippngThemeItem"; -import Button from "../form/Button"; +import { collection, getDocs, query, where } from "firebase/firestore"; import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import ErrorText from "../ErrorText"; import Loader from "../Loader"; +import SnippngThemeItem from "../profile/SnippngThemeItem"; +import { useAuth } from "@/context/AuthContext"; const PublishedThemeListing = () => { const [themes, setThemes] = useState([]); const [loadingThemes, setLoadingThemes] = useState(false); const router = useRouter(); + const { user } = useAuth(); const fetchPublishedThemes = async () => { 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. @@ -23,8 +24,10 @@ const PublishedThemeListing = () => { query(collection(db, "themes"), where("isPublished", "==", true)) ); docRef.forEach((doc) => { + const theme = doc.data(); + if (theme.ownerUid === user?.uid) return; // filter the own themes _themes.push({ - ...doc.data(), + ...theme, uid: doc.id, } as unknown as SnippngThemeAttributesInterface); }); @@ -37,8 +40,9 @@ const PublishedThemeListing = () => { }; useEffect(() => { + if (!user) return; fetchPublishedThemes(); - }, []); + }, [user]); if (loadingThemes) return ; diff --git a/components/profile/SnippngThemeItem.tsx b/components/profile/SnippngThemeItem.tsx index 8794d5d..c973487 100644 --- a/components/profile/SnippngThemeItem.tsx +++ b/components/profile/SnippngThemeItem.tsx @@ -1,22 +1,28 @@ import { useToast } from "@/context/ToastContext"; import { DEFAULT_BASE_SETUP, DEFAULT_CODE_SNIPPET } from "@/lib/constants"; import { SnippngThemeAttributesInterface } from "@/types"; -import { constructTheme, LocalStorage } from "@/utils"; +import { LocalStorage, constructTheme, deepClone } from "@/utils"; import { - TrashIcon, + ClipboardDocumentIcon, EllipsisHorizontalIcon, EyeIcon, EyeSlashIcon, + TrashIcon, } from "@heroicons/react/24/outline"; import { langs } from "@uiw/codemirror-extensions-langs"; import CodeMirror from "@uiw/react-codemirror"; -import React, { useState } from "react"; -import ErrorText from "../ErrorText"; import { db } from "@/config/firebase"; import { useAuth } from "@/context/AuthContext"; -import { deleteDoc, doc, updateDoc } from "firebase/firestore"; -import Loader from "../Loader"; +import { + addDoc, + collection, + deleteDoc, + doc, + updateDoc, +} from "firebase/firestore"; +import React, { useState } from "react"; +import ErrorText from "../ErrorText"; import Button from "../form/Button"; interface Props { @@ -92,6 +98,53 @@ const SnippngThemeItem: React.FC = ({ } }; + const forkTheme = async () => { + 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. + if (!user) return; + try { + let data = deepClone(theme); + // delete original theme's uid and id to persist them as they are unique + delete data.uid; + delete data.id; + const dataToBeAdded = { + ...data, // deep clone the theme to avoid mutation + ownerUid: user.uid, + isPublished: false, + owner: { + displayName: user?.displayName, + email: user?.email, + photoURL: user?.photoURL, + }, + }; + const savedDoc = await addDoc(collection(db, "themes"), { + ...dataToBeAdded, + }); + if (savedDoc.id) { + // get previously saved themes + let previousThemes = + (LocalStorage.get( + "local_themes" + ) as SnippngThemeAttributesInterface[]) || []; + + // push newly created theme inside the previous themes array + previousThemes.push({ + ...dataToBeAdded, + id: savedDoc.id, + }); + // store the newly created theme inside local storage + LocalStorage.set("local_themes", previousThemes); + + addToast({ + message: "Theme forked successfully!", + description: "You can view your forked themes in your profile", + }); + } + } catch (e) { + console.error("Error adding document: ", e); + } finally { + } + }; + if (!theme) return ( = ({ = ({ {theme.label} {theme?.ownerUid === user?.uid ? ( - + <> + + + ) : null} - @@ -197,6 +252,17 @@ const SnippngThemeItem: React.FC = ({ {theme?.owner?.email || "Snippng user"}

+ {theme?.ownerUid !== user?.uid ? ( + + ) : null}
diff --git a/layout/Header.tsx b/layout/Header.tsx index 46fdce0..6841dab 100644 --- a/layout/Header.tsx +++ b/layout/Header.tsx @@ -1,7 +1,10 @@ import { Button, Logo, SigninButton, ThemeToggle } from "@/components"; import { useAuth } from "@/context/AuthContext"; import { clsx } from "@/utils"; -import { ArrowLeftOnRectangleIcon } from "@heroicons/react/24/outline"; +import { + ArrowLeftOnRectangleIcon, + SparklesIcon, +} from "@heroicons/react/24/outline"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -17,7 +20,23 @@ const Header = () => {
- + {user?.uid ? ( <>
diff --git a/types/editor.ts b/types/editor.ts index d5e2b2b..5da13f2 100644 --- a/types/editor.ts +++ b/types/editor.ts @@ -9,6 +9,7 @@ export interface SelectOptionInterface { export type CustomTheme = ReturnType; export interface SnippngThemeAttributesInterface { id: string; + uid?: string; label: string; theme: "light" | "dark"; isPublished?: boolean; From 4cc8b5cb102734e458726e8b7864144c0d7cdbaf Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Sun, 28 May 2023 22:22:57 +0530 Subject: [PATCH 5/6] Add login check for cloning the theme --- components/explore/PublishedThemeListing.tsx | 1 - components/profile/SnippngThemeItem.tsx | 18 +++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/components/explore/PublishedThemeListing.tsx b/components/explore/PublishedThemeListing.tsx index db6ed04..b8623b2 100644 --- a/components/explore/PublishedThemeListing.tsx +++ b/components/explore/PublishedThemeListing.tsx @@ -40,7 +40,6 @@ const PublishedThemeListing = () => { }; useEffect(() => { - if (!user) return; fetchPublishedThemes(); }, [user]); diff --git a/components/profile/SnippngThemeItem.tsx b/components/profile/SnippngThemeItem.tsx index c973487..15f8741 100644 --- a/components/profile/SnippngThemeItem.tsx +++ b/components/profile/SnippngThemeItem.tsx @@ -98,9 +98,13 @@ const SnippngThemeItem: React.FC = ({ } }; - const forkTheme = async () => { + const cloneTheme = async () => { 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. - if (!user) return; + if (!user) + return addToast({ + message: "Login to clone the theme", + type: "error", + }); try { let data = deepClone(theme); // delete original theme's uid and id to persist them as they are unique @@ -135,8 +139,8 @@ const SnippngThemeItem: React.FC = ({ LocalStorage.set("local_themes", previousThemes); addToast({ - message: "Theme forked successfully!", - description: "You can view your forked themes in your profile", + message: "Theme cloned successfully!", + description: "You can view your cloned themes in your profile", }); } } catch (e) { @@ -255,10 +259,10 @@ const SnippngThemeItem: React.FC = ({ {theme?.ownerUid !== user?.uid ? ( From cc0b4512e45565f271a41aae68ab0cdc4e329d9e Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Tue, 20 Jun 2023 16:52:45 +0530 Subject: [PATCH 6/6] Add owner detail in on snippng code save --- components/editor/SnippngCodeArea.tsx | 6 +++ components/explore/PublishedThemeListing.tsx | 1 - pages/snippet/[uid].tsx | 43 +++++++++++++++++++- types/editor.ts | 1 + utils/index.ts | 1 + 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/components/editor/SnippngCodeArea.tsx b/components/editor/SnippngCodeArea.tsx index 6ed76c7..4b97339 100644 --- a/components/editor/SnippngCodeArea.tsx +++ b/components/editor/SnippngCodeArea.tsx @@ -83,6 +83,11 @@ const SnippngCodeArea: React.FC = ({ underConstructionTheme }) => { const savedDoc = await addDoc(collection(db, "snippets"), { ...dataToBeAdded, ownerUid: user.uid, + owner: { + displayName: user.displayName, + photoURL: user.photoURL, + email: user.email, + }, }); if (savedDoc.id) { addToast({ @@ -148,6 +153,7 @@ const SnippngCodeArea: React.FC = ({ underConstructionTheme }) => { ...editorConfig, uid: undefined, ownerUid: undefined, + owner: undefined, }); }, [editorConfig, uid, underConstructionTheme]); diff --git a/components/explore/PublishedThemeListing.tsx b/components/explore/PublishedThemeListing.tsx index b8623b2..804877a 100644 --- a/components/explore/PublishedThemeListing.tsx +++ b/components/explore/PublishedThemeListing.tsx @@ -25,7 +25,6 @@ const PublishedThemeListing = () => { ); docRef.forEach((doc) => { const theme = doc.data(); - if (theme.ownerUid === user?.uid) return; // filter the own themes _themes.push({ ...theme, uid: doc.id, diff --git a/pages/snippet/[uid].tsx b/pages/snippet/[uid].tsx index ea68a07..f3efe3d 100644 --- a/pages/snippet/[uid].tsx +++ b/pages/snippet/[uid].tsx @@ -13,7 +13,7 @@ import { useEffect, useState } from "react"; // TODO: implement SSR const SavedSnippet = () => { const router = useRouter(); - const { setEditorConfig } = useSnippngEditor(); + const { editorConfig, setEditorConfig } = useSnippngEditor(); const [notFound, setNotFound] = useState(false); const [loadingConfig, setLoadingConfig] = useState(true); @@ -53,6 +53,7 @@ const SavedSnippet = () => { ...defaultEditorConfig, uid: undefined, ownerUid: undefined, + owner: undefined, }); }; }, [router.isReady]); @@ -83,7 +84,45 @@ const SavedSnippet = () => { ); - return {loadingConfig ? : }; + return ( + + {loadingConfig ? ( + + ) : ( + <> +
+
+
+
+ +
+
+
+

+ {editorConfig?.owner?.displayName || "Snippng user"}{" "} + + Author + +

+

+ {editorConfig?.owner?.email || "Snippng user email"} +

+
+
+
+ + + )} +
+ ); }; export default SavedSnippet; diff --git a/types/editor.ts b/types/editor.ts index 5da13f2..1196aab 100644 --- a/types/editor.ts +++ b/types/editor.ts @@ -54,6 +54,7 @@ export type SnippngWindowControlsType = export interface SnippngEditorConfigInterface { ownerUid?: string; + owner?: Pick; uid?: string; watermark?: boolean; code: string; diff --git a/utils/index.ts b/utils/index.ts index 90423d3..e75ea58 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -175,6 +175,7 @@ export const getExportableConfig = ( deepClonedConfig.bgImageVisiblePatch = null; delete deepClonedConfig.uid; delete deepClonedConfig.ownerUid; + delete deepClonedConfig.owner; const exportableConfig: SnippngExportableConfig = deepClonedConfig; return exportableConfig; };