Skip to content

Commit c5c2df5

Browse files
committed
Add explore theme nav link with clone theme functionality
1 parent e4fb51b commit c5c2df5

File tree

4 files changed

+160
-69
lines changed

4 files changed

+160
-69
lines changed

components/explore/PublishedThemeListing.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { db } from "@/config/firebase";
22
import { SnippngThemeAttributesInterface } from "@/types";
3-
import { collection, getDocs, query, where } from "firebase/firestore";
4-
import React, { useEffect, useState } from "react";
5-
import ErrorText from "../ErrorText";
63
import { SparklesIcon } from "@heroicons/react/24/outline";
7-
import SnippngThemeItem from "../profile/SnippngThemeItem";
8-
import Button from "../form/Button";
4+
import { collection, getDocs, query, where } from "firebase/firestore";
95
import { useRouter } from "next/router";
6+
import { useEffect, useState } from "react";
7+
import ErrorText from "../ErrorText";
108
import Loader from "../Loader";
9+
import SnippngThemeItem from "../profile/SnippngThemeItem";
10+
import { useAuth } from "@/context/AuthContext";
1111

1212
const PublishedThemeListing = () => {
1313
const [themes, setThemes] = useState<SnippngThemeAttributesInterface[]>([]);
1414
const [loadingThemes, setLoadingThemes] = useState(false);
1515
const router = useRouter();
16+
const { user } = useAuth();
1617

1718
const fetchPublishedThemes = async () => {
1819
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 = () => {
2324
query(collection(db, "themes"), where("isPublished", "==", true))
2425
);
2526
docRef.forEach((doc) => {
27+
const theme = doc.data();
28+
if (theme.ownerUid === user?.uid) return; // filter the own themes
2629
_themes.push({
27-
...doc.data(),
30+
...theme,
2831
uid: doc.id,
2932
} as unknown as SnippngThemeAttributesInterface);
3033
});
@@ -37,8 +40,9 @@ const PublishedThemeListing = () => {
3740
};
3841

3942
useEffect(() => {
43+
if (!user) return;
4044
fetchPublishedThemes();
41-
}, []);
45+
}, [user]);
4246

4347
if (loadingThemes) return <Loader />;
4448

components/profile/SnippngThemeItem.tsx

Lines changed: 126 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import { useToast } from "@/context/ToastContext";
22
import { DEFAULT_BASE_SETUP, DEFAULT_CODE_SNIPPET } from "@/lib/constants";
33
import { SnippngThemeAttributesInterface } from "@/types";
4-
import { constructTheme, LocalStorage } from "@/utils";
4+
import { LocalStorage, constructTheme, deepClone } from "@/utils";
55
import {
6-
TrashIcon,
6+
ClipboardDocumentIcon,
77
EllipsisHorizontalIcon,
88
EyeIcon,
99
EyeSlashIcon,
10+
TrashIcon,
1011
} from "@heroicons/react/24/outline";
1112
import { langs } from "@uiw/codemirror-extensions-langs";
1213
import CodeMirror from "@uiw/react-codemirror";
1314

14-
import React, { useState } from "react";
15-
import ErrorText from "../ErrorText";
1615
import { db } from "@/config/firebase";
1716
import { useAuth } from "@/context/AuthContext";
18-
import { deleteDoc, doc, updateDoc } from "firebase/firestore";
19-
import Loader from "../Loader";
17+
import {
18+
addDoc,
19+
collection,
20+
deleteDoc,
21+
doc,
22+
updateDoc,
23+
} from "firebase/firestore";
24+
import React, { useState } from "react";
25+
import ErrorText from "../ErrorText";
2026
import Button from "../form/Button";
2127

2228
interface Props {
@@ -92,6 +98,53 @@ const SnippngThemeItem: React.FC<Props> = ({
9298
}
9399
};
94100

101+
const forkTheme = async () => {
102+
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.
103+
if (!user) return;
104+
try {
105+
let data = deepClone(theme);
106+
// delete original theme's uid and id to persist them as they are unique
107+
delete data.uid;
108+
delete data.id;
109+
const dataToBeAdded = {
110+
...data, // deep clone the theme to avoid mutation
111+
ownerUid: user.uid,
112+
isPublished: false,
113+
owner: {
114+
displayName: user?.displayName,
115+
email: user?.email,
116+
photoURL: user?.photoURL,
117+
},
118+
};
119+
const savedDoc = await addDoc(collection(db, "themes"), {
120+
...dataToBeAdded,
121+
});
122+
if (savedDoc.id) {
123+
// get previously saved themes
124+
let previousThemes =
125+
(LocalStorage.get(
126+
"local_themes"
127+
) as SnippngThemeAttributesInterface[]) || [];
128+
129+
// push newly created theme inside the previous themes array
130+
previousThemes.push({
131+
...dataToBeAdded,
132+
id: savedDoc.id,
133+
});
134+
// store the newly created theme inside local storage
135+
LocalStorage.set("local_themes", previousThemes);
136+
137+
addToast({
138+
message: "Theme forked successfully!",
139+
description: "You can view your forked themes in your profile",
140+
});
141+
}
142+
} catch (e) {
143+
console.error("Error adding document: ", e);
144+
} finally {
145+
}
146+
};
147+
95148
if (!theme)
96149
return (
97150
<ErrorText
@@ -103,7 +156,7 @@ const SnippngThemeItem: React.FC<Props> = ({
103156
<CodeMirror
104157
readOnly
105158
editable={false}
106-
className="CodeMirror__Theme__Preview__Editor"
159+
className={"CodeMirror__Theme__Preview__Editor"}
107160
value={DEFAULT_CODE_SNIPPET}
108161
extensions={[langs.javascript()]}
109162
basicSetup={{
@@ -127,60 +180,62 @@ const SnippngThemeItem: React.FC<Props> = ({
127180
{theme.label}
128181
</span>
129182
{theme?.ownerUid === user?.uid ? (
130-
<button
131-
aria-label="delete-theme"
132-
title="Delete theme"
133-
disabled={deletingTheme}
134-
onClick={() => {
135-
let ok = confirm(
136-
`Are you sure you want to ${
137-
theme.isPublished ? "unpublish" : "publish"
138-
} this theme?`
139-
);
140-
if (!ok) return;
141-
togglePublishThemeItem(theme.id);
142-
}}
143-
className="inline-flex ml-auto mr-2 items-center gap-2 dark:text-white text-zinc-700 outline outline-[1px] dark:outline-zinc-500 rounded-md outline-zinc-200"
144-
>
145-
{theme.isPublished ? (
146-
<EyeIcon
147-
title="Theme is published"
148-
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
149-
/>
150-
) : (
151-
<EyeSlashIcon
152-
role={"button"}
153-
title="Theme is private"
154-
tabIndex={0}
155-
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
156-
/>
157-
)}
158-
</button>
183+
<>
184+
<button
185+
aria-label="delete-theme"
186+
title="Delete theme"
187+
disabled={deletingTheme}
188+
onClick={() => {
189+
let ok = confirm(
190+
`Are you sure you want to ${
191+
theme.isPublished ? "unpublish" : "publish"
192+
} this theme?`
193+
);
194+
if (!ok) return;
195+
togglePublishThemeItem(theme.id);
196+
}}
197+
className="inline-flex ml-auto mr-2 items-center gap-2 dark:text-white text-zinc-700 outline outline-[1px] dark:outline-zinc-500 rounded-md outline-zinc-200"
198+
>
199+
{theme.isPublished ? (
200+
<EyeIcon
201+
title="Theme is published"
202+
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
203+
/>
204+
) : (
205+
<EyeSlashIcon
206+
role={"button"}
207+
title="Theme is private"
208+
tabIndex={0}
209+
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
210+
/>
211+
)}
212+
</button>
213+
<button
214+
aria-label="delete-theme"
215+
title="Delete theme"
216+
disabled={deletingTheme}
217+
onClick={() => {
218+
let ok = confirm(
219+
"Are you sure you want to delete this theme permanently?"
220+
);
221+
if (!ok) return;
222+
deleteThemeItem(theme.id);
223+
}}
224+
className="inline-flex items-center gap-2 dark:text-white text-zinc-700 outline outline-[1px] dark:outline-zinc-500 rounded-md outline-zinc-200"
225+
>
226+
{/* <PencilIcon className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer" /> */}
227+
{deletingTheme ? (
228+
<EllipsisHorizontalIcon className="animate-pulse h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer" />
229+
) : (
230+
<TrashIcon
231+
role={"button"}
232+
tabIndex={0}
233+
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
234+
/>
235+
)}
236+
</button>
237+
</>
159238
) : null}
160-
<button
161-
aria-label="delete-theme"
162-
title="Delete theme"
163-
disabled={deletingTheme}
164-
onClick={() => {
165-
let ok = confirm(
166-
"Are you sure you want to delete this theme permanently?"
167-
);
168-
if (!ok) return;
169-
deleteThemeItem(theme.id);
170-
}}
171-
className="inline-flex items-center gap-2 dark:text-white text-zinc-700 outline outline-[1px] dark:outline-zinc-500 rounded-md outline-zinc-200"
172-
>
173-
{/* <PencilIcon className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer" /> */}
174-
{deletingTheme ? (
175-
<EllipsisHorizontalIcon className="animate-pulse h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer" />
176-
) : (
177-
<TrashIcon
178-
role={"button"}
179-
tabIndex={0}
180-
className="h-7 w-7 hover:dark:bg-zinc-600 hover:bg-zinc-200 p-1 rounded-md cursor-pointer"
181-
/>
182-
)}
183-
</button>
184239
</span>
185240
<span className="block w-full h-[0.1px] dark:bg-zinc-500 bg-zinc-300 my-3"></span>
186241
<span className="flex justify-start gap-3 items-center w-full">
@@ -197,6 +252,17 @@ const SnippngThemeItem: React.FC<Props> = ({
197252
{theme?.owner?.email || "Snippng user"}
198253
</p>
199254
</span>
255+
{theme?.ownerUid !== user?.uid ? (
256+
<Button
257+
className="ml-auto"
258+
aria-label="fork-theme"
259+
title="Fork theme"
260+
StartIcon={ClipboardDocumentIcon}
261+
onClick={forkTheme}
262+
>
263+
Clone theme
264+
</Button>
265+
) : null}
200266
</span>
201267
</div>
202268
</CodeMirror>

layout/Header.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Button, Logo, SigninButton, ThemeToggle } from "@/components";
22
import { useAuth } from "@/context/AuthContext";
33
import { clsx } from "@/utils";
4-
import { ArrowLeftOnRectangleIcon } from "@heroicons/react/24/outline";
4+
import {
5+
ArrowLeftOnRectangleIcon,
6+
SparklesIcon,
7+
} from "@heroicons/react/24/outline";
58
import Link from "next/link";
69
import { useRouter } from "next/router";
710

@@ -17,7 +20,23 @@ const Header = () => {
1720
</Link>
1821
</div>
1922
<div className="flex z-40 justify-end gap-4 items-center w-1/2 flex-shrink-0">
20-
<ThemeToggle />
23+
<Button
24+
data-testid="explore-themes"
25+
StartIcon={(props) => (
26+
<SparklesIcon
27+
{...props}
28+
className={clsx(
29+
"md:mr-2 md:!w-4 md:!h-4 !w-6 !h-6 mr-0",
30+
props.className ?? ""
31+
)}
32+
/>
33+
)}
34+
onClick={() => {
35+
router.push(`/explore/themes`);
36+
}}
37+
>
38+
<span className="md:block hidden">Explore themes</span>
39+
</Button>
2140
{user?.uid ? (
2241
<>
2342
<Button
@@ -60,6 +79,7 @@ const Header = () => {
6079
) : (
6180
<SigninButton />
6281
)}
82+
<ThemeToggle />
6383
</div>
6484
</nav>
6585
</header>

types/editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface SelectOptionInterface {
99
export type CustomTheme = ReturnType<typeof createTheme>;
1010
export interface SnippngThemeAttributesInterface {
1111
id: string;
12+
uid?: string;
1213
label: string;
1314
theme: "light" | "dark";
1415
isPublished?: boolean;

0 commit comments

Comments
 (0)