|
|
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"; |
|
|
import { useMemo } from "react"; |
|
|
import { useLocalStorage, useMount, useUpdateEffect } from "react-use"; |
|
|
import { toast } from "sonner"; |
|
|
import { useRouter } from "next/navigation"; |
|
|
|
|
|
import { defaultHTML } from "@/lib/consts"; |
|
|
import { Commit, Page, Project } from "@/types"; |
|
|
import { api } from "@/lib/api"; |
|
|
import { isTheSameHtml } from "@/lib/compare-html-diff"; |
|
|
|
|
|
export const useEditor = (namespace?: string, repoId?: string) => { |
|
|
const client = useQueryClient(); |
|
|
const router = useRouter(); |
|
|
|
|
|
const { data: project, isFetching: isLoadingProject } = useQuery({ |
|
|
queryKey: ["editor.project"], |
|
|
queryFn: async () => { |
|
|
try { |
|
|
const response = await api.get(`/me/projects/${namespace}/${repoId}`); |
|
|
const { project, pages, files, commits } = response.data; |
|
|
if (pages?.length > 0) { |
|
|
setPages(pages); |
|
|
} |
|
|
if (files?.length > 0) { |
|
|
setFiles(files); |
|
|
} |
|
|
if (commits?.length > 0) { |
|
|
setCommits(commits); |
|
|
} |
|
|
return project; |
|
|
} catch (error: any) { |
|
|
toast.error(error.response.data.error); |
|
|
router.push("/"); |
|
|
return null; |
|
|
} |
|
|
}, |
|
|
retry: false, |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: 'always', |
|
|
staleTime: 0, |
|
|
gcTime: 0, |
|
|
enabled: !!namespace && !!repoId, |
|
|
}); |
|
|
const setProject = (newProject: any) => { |
|
|
const { project, pages, files, commits } = newProject; |
|
|
if (pages?.length > 0) { |
|
|
setPages(pages); |
|
|
} |
|
|
if (files?.length > 0) { |
|
|
setFiles(files); |
|
|
} |
|
|
if (commits?.length > 0) { |
|
|
setCommits(commits); |
|
|
} |
|
|
client.setQueryData(["editor.project"], project); |
|
|
}; |
|
|
|
|
|
const { data: pages = [] } = useQuery<Page[]>({ |
|
|
queryKey: ["editor.pages"], |
|
|
queryFn: async (): Promise<Page[]> => { |
|
|
return [ |
|
|
{ |
|
|
path: "index.html", |
|
|
html: defaultHTML, |
|
|
}, |
|
|
]; |
|
|
}, |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
retry: false, |
|
|
initialData: [ |
|
|
{ |
|
|
path: "index.html", |
|
|
html: defaultHTML, |
|
|
}, |
|
|
], |
|
|
}); |
|
|
const setPages = (newPages: Page[] | ((prev: Page[]) => Page[])) => { |
|
|
if (typeof newPages === "function") { |
|
|
const currentPages = client.getQueryData<Page[]>(["editor.pages"]) ?? []; |
|
|
client.setQueryData(["editor.pages"], newPages(currentPages)); |
|
|
} else { |
|
|
client.setQueryData(["editor.pages"], newPages); |
|
|
} |
|
|
}; |
|
|
|
|
|
const { data: currentPage = "index.html" } = useQuery({ |
|
|
queryKey: ["editor.currentPage"], |
|
|
queryFn: async () => "index.html", |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
}); |
|
|
const setCurrentPage = (newCurrentPage: string) => { |
|
|
client.setQueryData(["editor.currentPage"], newCurrentPage); |
|
|
}; |
|
|
|
|
|
const { data: prompts = [] } = useQuery({ |
|
|
queryKey: ["editor.prompts"], |
|
|
queryFn: async () => [], |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
retry: false, |
|
|
initialData: [], |
|
|
}); |
|
|
const setPrompts = (newPrompts: string[] | ((prev: string[]) => string[])) => { |
|
|
if (typeof newPrompts === "function") { |
|
|
const currentPrompts = client.getQueryData<string[]>(["editor.prompts"]) ?? []; |
|
|
client.setQueryData(["editor.prompts"], newPrompts(currentPrompts)); |
|
|
} else { |
|
|
client.setQueryData(["editor.prompts"], newPrompts); |
|
|
} |
|
|
}; |
|
|
|
|
|
const { data: files = [] } = useQuery({ |
|
|
queryKey: ["editor.files"], |
|
|
queryFn: async () => [], |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
retry: false, |
|
|
initialData: [], |
|
|
}); |
|
|
const setFiles = (newFiles: string[] | ((prev: string[]) => string[])) => { |
|
|
if (typeof newFiles === "function") { |
|
|
const currentFiles = client.getQueryData<string[]>(["editor.files"]) ?? []; |
|
|
client.setQueryData(["editor.files"], newFiles(currentFiles)); |
|
|
} else { |
|
|
client.setQueryData(["editor.files"], newFiles); |
|
|
} |
|
|
}; |
|
|
|
|
|
const { data: commits = [] } = useQuery({ |
|
|
queryKey: ["editor.commits"], |
|
|
queryFn: async () => [], |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
initialData: [], |
|
|
}); |
|
|
const setCommits = (newCommits: Commit[] | ((prev: Commit[]) => Commit[])) => { |
|
|
if (typeof newCommits === "function") { |
|
|
const currentCommits = client.getQueryData<Commit[]>(["editor.commits"]) ?? []; |
|
|
client.setQueryData(["editor.commits"], newCommits(currentCommits)); |
|
|
} else { |
|
|
client.setQueryData(["editor.commits"], newCommits); |
|
|
} |
|
|
}; |
|
|
|
|
|
const { data: device = "desktop" } = useQuery<string>({ |
|
|
queryKey: ["editor.device"], |
|
|
queryFn: async () => "desktop", |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
initialData: "desktop", |
|
|
}); |
|
|
const setDevice = (newDevice: string | ((prev: string) => string)) => { |
|
|
client.setQueryData(["editor.device"], newDevice); |
|
|
}; |
|
|
|
|
|
const { data: currentTab = "chat" } = useQuery({ |
|
|
queryKey: ["editor.currentTab"], |
|
|
queryFn: async () => "chat", |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
}); |
|
|
const setCurrentTab = (newCurrentTab: string | ((prev: string) => string)) => { |
|
|
client.setQueryData(["editor.currentTab"], newCurrentTab); |
|
|
}; |
|
|
|
|
|
const { data: currentCommit = null } = useQuery<string | null>({ |
|
|
queryKey: ["editor.currentCommit"], |
|
|
queryFn: async () => null, |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
}); |
|
|
const setCurrentCommit = (newCurrentCommit: string | null) => { |
|
|
client.setQueryData(["editor.currentCommit"], newCurrentCommit); |
|
|
}; |
|
|
|
|
|
const currentPageData = useMemo(() => { |
|
|
return pages.find((page) => page.path === currentPage) ?? { path: "index.html", html: defaultHTML }; |
|
|
}, [pages, currentPage]); |
|
|
|
|
|
const uploadFilesMutation = useMutation({ |
|
|
mutationFn: async ({ files, project }: { files: FileList; project: Project }) => { |
|
|
const images = Array.from(files).filter((file) => { |
|
|
return file.type.startsWith("image/"); |
|
|
}); |
|
|
|
|
|
const data = new FormData(); |
|
|
images.forEach((image) => { |
|
|
data.append("images", image); |
|
|
}); |
|
|
|
|
|
const response = await fetch( |
|
|
`/api/me/projects/${project.space_id}/images`, |
|
|
{ |
|
|
method: "POST", |
|
|
body: data, |
|
|
} |
|
|
); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error('Upload failed'); |
|
|
} |
|
|
|
|
|
return response.json(); |
|
|
}, |
|
|
onSuccess: (data) => { |
|
|
setFiles((prev) => [...prev, ...data.uploadedFiles]); |
|
|
}, |
|
|
}); |
|
|
|
|
|
const uploadFiles = (files: FileList | null, project: Project) => { |
|
|
if (!files || !project) return; |
|
|
uploadFilesMutation.mutate({ files, project }); |
|
|
}; |
|
|
|
|
|
|
|
|
const { data: lastSavedPages = [] } = useQuery<Page[]>({ |
|
|
queryKey: ["editor.lastSavedPages"], |
|
|
queryFn: async () => [], |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
initialData: [], |
|
|
}); |
|
|
const setLastSavedPages = (newPages: Page[]) => { |
|
|
client.setQueryData(["editor.lastSavedPages"], newPages); |
|
|
}; |
|
|
|
|
|
const { data: hasUnsavedChanges = false } = useQuery({ |
|
|
queryKey: ["editor.hasUnsavedChanges"], |
|
|
queryFn: async () => false, |
|
|
refetchOnWindowFocus: false, |
|
|
refetchOnReconnect: false, |
|
|
refetchOnMount: false, |
|
|
}); |
|
|
const setHasUnsavedChanges = (hasChanges: boolean) => { |
|
|
client.setQueryData(["editor.hasUnsavedChanges"], hasChanges); |
|
|
}; |
|
|
|
|
|
|
|
|
const saveChangesMutation = useMutation({ |
|
|
mutationFn: async ({ pages, project, namespace, repoId }: { pages: Page[]; project: any; namespace?: string; repoId?: string }) => { |
|
|
if (!project?.space_id || !namespace || !repoId) { |
|
|
throw new Error("Project not found or missing parameters"); |
|
|
} |
|
|
|
|
|
const response = await api.put(`/me/projects/${namespace}/${repoId}/save`, { |
|
|
pages, |
|
|
commitTitle: "Manual changes saved" |
|
|
}); |
|
|
|
|
|
if (!response.data.ok) { |
|
|
throw new Error(response.data.message || "Failed to save changes"); |
|
|
} |
|
|
|
|
|
return response.data; |
|
|
}, |
|
|
onSuccess: (data) => { |
|
|
setLastSavedPages([...pages]); |
|
|
setHasUnsavedChanges(false); |
|
|
if (data.commit) { |
|
|
setCommits((prev) => [data.commit, ...prev]); |
|
|
} |
|
|
}, |
|
|
}); |
|
|
|
|
|
const saveChanges = async () => { |
|
|
if (!project || !hasUnsavedChanges || !namespace || !repoId) return; |
|
|
return saveChangesMutation.mutateAsync({ pages, project, namespace, repoId }); |
|
|
}; |
|
|
|
|
|
// Check for unsaved changes when pages change |
|
|
const checkForUnsavedChanges = () => { |
|
|
if (pages.length === 0 || lastSavedPages.length === 0) return; |
|
|
|
|
|
const hasChanges = JSON.stringify(pages) !== JSON.stringify(lastSavedPages); |
|
|
setHasUnsavedChanges(hasChanges); |
|
|
}; |
|
|
|
|
|
|
|
|
useUpdateEffect(() => { |
|
|
if (project && pages.length > 0 && lastSavedPages.length === 0) { |
|
|
setLastSavedPages([...pages]); |
|
|
} |
|
|
}, [project, pages]); |
|
|
|
|
|
|
|
|
useUpdateEffect(() => { |
|
|
if (lastSavedPages.length > 0) { |
|
|
checkForUnsavedChanges(); |
|
|
} |
|
|
}, [pages, lastSavedPages]); |
|
|
|
|
|
useUpdateEffect(() => { |
|
|
if (namespace && repoId) { |
|
|
|
|
|
setHasUnsavedChanges(false); |
|
|
setLastSavedPages([]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.invalidateQueries({ queryKey: ["editor.currentCommit"] }); |
|
|
client.invalidateQueries({ queryKey: ["editor.lastSavedPages"] }); |
|
|
client.invalidateQueries({ queryKey: ["editor.hasUnsavedChanges"] }); |
|
|
} |
|
|
}, [namespace, repoId]) |
|
|
|
|
|
const isSameHtml = useMemo(() => { |
|
|
return isTheSameHtml(currentPageData.html); |
|
|
}, [pages]); |
|
|
|
|
|
return { |
|
|
isLoadingProject, |
|
|
project, |
|
|
prompts, |
|
|
pages, |
|
|
setPages, |
|
|
setPrompts, |
|
|
files, |
|
|
setFiles, |
|
|
device, |
|
|
setDevice, |
|
|
currentPage, |
|
|
setCurrentPage, |
|
|
currentPageData, |
|
|
currentTab, |
|
|
setCurrentTab, |
|
|
uploadFiles, |
|
|
commits, |
|
|
setCommits, |
|
|
currentCommit, |
|
|
setCurrentCommit, |
|
|
setProject, |
|
|
isSameHtml, |
|
|
isUploading: uploadFilesMutation.isPending, |
|
|
globalEditorLoading: uploadFilesMutation.isPending || isLoadingProject, |
|
|
|
|
|
hasUnsavedChanges, |
|
|
saveChanges, |
|
|
isSaving: saveChangesMutation.isPending, |
|
|
lastSavedPages, |
|
|
setLastSavedPages, |
|
|
}; |
|
|
}; |
|
|
|