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 ( -
+
= ({ 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/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/explore/PublishedThemeListing.tsx b/components/explore/PublishedThemeListing.tsx new file mode 100644 index 0000000..804877a --- /dev/null +++ b/components/explore/PublishedThemeListing.tsx @@ -0,0 +1,96 @@ +import { db } from "@/config/firebase"; +import { SnippngThemeAttributesInterface } from "@/types"; +import { SparklesIcon } from "@heroicons/react/24/outline"; +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. + setLoadingThemes(true); + try { + const _themes: SnippngThemeAttributesInterface[] = []; + const docRef = await getDocs( + query(collection(db, "themes"), where("isPublished", "==", true)) + ); + docRef.forEach((doc) => { + const theme = doc.data(); + _themes.push({ + ...theme, + uid: doc.id, + } as unknown as SnippngThemeAttributesInterface); + }); + setThemes(_themes); + } catch (error) { + console.log("Error fetching snippets", error); + } finally { + setLoadingThemes(false); + } + }; + + useEffect(() => { + fetchPublishedThemes(); + }, [user]); + + if (loadingThemes) return ; + + return ( +
+
+
+
+ {themes.length ? ( +
    + {themes.map((theme) => ( + { + setThemes( + [...themes].filter((thm) => thm.id !== themeId) + ); + }} + onPublishChange={(themeId) => { + setThemes( + [...themes].map((theme) => { + if (theme.id === themeId) { + theme.isPublished = !theme.isPublished; + } + return theme; + }) + ); + }} + /> + ))} +
+ ) : ( + { + router.push("/theme/create"); + }, + }} + /> + )} +
+
+
+
+ ); +}; + +export default PublishedThemeListing; diff --git a/components/profile/SnippngThemeItem.tsx b/components/profile/SnippngThemeItem.tsx index a8fc3cc..15f8741 100644 --- a/components/profile/SnippngThemeItem.tsx +++ b/components/profile/SnippngThemeItem.tsx @@ -1,24 +1,41 @@ 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 { LocalStorage, constructTheme, deepClone } from "@/utils"; +import { + 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 } 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 { 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 +60,95 @@ 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); + } + }; + + 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 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 + 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 cloned successfully!", + description: "You can view your cloned themes in your profile", + }); + } + } catch (e) { + console.error("Error adding document: ", e); + } finally { + } + }; + if (!theme) return ( = ({ theme, onDelete }) => { = ({ theme, onDelete }) => { theme={constructTheme(theme)} indentWithTab > - {theme.isPublic ? ( - + {theme.isPublished ? ( + Published ) : null} @@ -83,30 +183,90 @@ const SnippngThemeItem: React.FC = ({ theme, onDelete }) => { {theme.label} - + {theme?.ownerUid === user?.uid ? ( + <> + + + + ) : null} + + + + + +

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

+

+ {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/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/pages/profile.tsx b/pages/profile.tsx index 6fa4607..2fdb409 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -193,6 +193,17 @@ const UserProfile = () => { ) ); }} + onPublishChange={(themeId) => { + setSavedThemes( + [...savedThemes].map((theme) => { + if (theme.id === themeId) { + theme.isPublished = + !theme.isPublished; + } + return theme; + }) + ); + }} /> ))} 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/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 1019332..1196aab 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; @@ -8,9 +9,12 @@ export interface SelectOptionInterface { export type CustomTheme = ReturnType; export interface SnippngThemeAttributesInterface { id: string; + uid?: string; label: string; theme: "light" | "dark"; - isPublic?: boolean; + isPublished?: boolean; + owner?: Pick; + ownerUid?: string; config: { background: string; foreground: string; @@ -50,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; };