11import { useToast } from "@/context/ToastContext" ;
22import { DEFAULT_BASE_SETUP , DEFAULT_CODE_SNIPPET } from "@/lib/constants" ;
33import { SnippngThemeAttributesInterface } from "@/types" ;
4- import { constructTheme , LocalStorage } from "@/utils" ;
4+ import { LocalStorage , constructTheme , deepClone } from "@/utils" ;
55import {
6- TrashIcon ,
6+ ClipboardDocumentIcon ,
77 EllipsisHorizontalIcon ,
88 EyeIcon ,
99 EyeSlashIcon ,
10+ TrashIcon ,
1011} from "@heroicons/react/24/outline" ;
1112import { langs } from "@uiw/codemirror-extensions-langs" ;
1213import CodeMirror from "@uiw/react-codemirror" ;
1314
14- import React , { useState } from "react" ;
15- import ErrorText from "../ErrorText" ;
1615import { db } from "@/config/firebase" ;
1716import { 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" ;
2026import Button from "../form/Button" ;
2127
2228interface 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 >
0 commit comments