enzostvs HF Staff commited on
Commit
0b4acc5
·
1 Parent(s): 2142508

Load existing project

Browse files
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -41,11 +41,11 @@ export async function GET(
41
  additionalFields: ["author"],
42
  });
43
 
44
- if (!space || space.sdk !== "static" || space.private) {
45
  return NextResponse.json(
46
  {
47
  ok: false,
48
- error: "Space is not a static space or is private",
49
  },
50
  { status: 404 }
51
  );
@@ -160,3 +160,76 @@ export async function PUT(
160
  );
161
  return NextResponse.json({ ok: true }, { status: 200 });
162
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  additionalFields: ["author"],
42
  });
43
 
44
+ if (!space || space.sdk !== "static") {
45
  return NextResponse.json(
46
  {
47
  ok: false,
48
+ error: "Space is not a static space",
49
  },
50
  { status: 404 }
51
  );
 
160
  );
161
  return NextResponse.json({ ok: true }, { status: 200 });
162
  }
163
+
164
+ export async function POST(
165
+ req: NextRequest,
166
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
167
+ ) {
168
+ const user = await isAuthenticated();
169
+
170
+ if (user instanceof NextResponse || !user) {
171
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
172
+ }
173
+
174
+ await dbConnect();
175
+ const param = await params;
176
+ const { namespace, repoId } = param;
177
+
178
+ const space = await spaceInfo({
179
+ name: namespace + "/" + repoId,
180
+ accessToken: user.token as string,
181
+ additionalFields: ["author"],
182
+ });
183
+
184
+ if (!space || space.sdk !== "static") {
185
+ return NextResponse.json(
186
+ {
187
+ ok: false,
188
+ error: "Space is not a static space",
189
+ },
190
+ { status: 404 }
191
+ );
192
+ }
193
+ if (space.author !== user.name) {
194
+ return NextResponse.json(
195
+ {
196
+ ok: false,
197
+ error: "Space does not belong to the authenticated user",
198
+ },
199
+ { status: 403 }
200
+ );
201
+ }
202
+
203
+ const project = await Project.findOne({
204
+ user_id: user.id,
205
+ space_id: `${namespace}/${repoId}`,
206
+ }).lean();
207
+ if (project) {
208
+ return NextResponse.json(
209
+ {
210
+ ok: false,
211
+ error: "Project already exists",
212
+ },
213
+ { status: 400 }
214
+ );
215
+ }
216
+
217
+ const newProject = new Project({
218
+ user_id: user.id,
219
+ space_id: `${namespace}/${repoId}`,
220
+ prompts: [],
221
+ });
222
+
223
+ await newProject.save();
224
+ return NextResponse.json(
225
+ {
226
+ ok: true,
227
+ project: {
228
+ id: newProject._id,
229
+ space_id: newProject.space_id,
230
+ prompts: newProject.prompts,
231
+ },
232
+ },
233
+ { status: 201 }
234
+ );
235
+ }
components/editor/index.tsx CHANGED
@@ -3,7 +3,7 @@ import { useRef, useState } from "react";
3
  import { toast } from "sonner";
4
  import { editor } from "monaco-editor";
5
  import Editor from "@monaco-editor/react";
6
- import { CopyIcon } from "lucide-react";
7
  import {
8
  useCopyToClipboard,
9
  useEvent,
@@ -24,10 +24,13 @@ import { AskAI } from "@/components/editor/ask-ai";
24
  import { DeployButton } from "./deploy-button";
25
  import { Project } from "@/types";
26
  import { SaveButton } from "./save-button";
 
 
27
 
28
  export const AppEditor = ({ project }: { project?: Project | null }) => {
29
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
30
  const [, copyToClipboard] = useCopyToClipboard();
 
31
  const { html, setHtml, htmlHistory, setHtmlHistory, prompts, setPrompts } =
32
  useEditor(project?.html ?? (htmlStorage as string) ?? defaultHTML);
33
  // get query params from URL
@@ -173,6 +176,16 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
173
  return (
174
  <section className="h-[100dvh] bg-neutral-950 flex flex-col">
175
  <Header tab={currentTab} onNewTab={setCurrentTab}>
 
 
 
 
 
 
 
 
 
 
176
  {project?._id ? (
177
  <SaveButton html={html} prompts={prompts} />
178
  ) : (
 
3
  import { toast } from "sonner";
4
  import { editor } from "monaco-editor";
5
  import Editor from "@monaco-editor/react";
6
+ import { CopyIcon, Import } from "lucide-react";
7
  import {
8
  useCopyToClipboard,
9
  useEvent,
 
24
  import { DeployButton } from "./deploy-button";
25
  import { Project } from "@/types";
26
  import { SaveButton } from "./save-button";
27
+ import { Button } from "@/components/ui/button";
28
+ import { useUser } from "@/hooks/useUser";
29
 
30
  export const AppEditor = ({ project }: { project?: Project | null }) => {
31
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
32
  const [, copyToClipboard] = useCopyToClipboard();
33
+ const { user, openLoginWindow } = useUser();
34
  const { html, setHtml, htmlHistory, setHtmlHistory, prompts, setPrompts } =
35
  useEditor(project?.html ?? (htmlStorage as string) ?? defaultHTML);
36
  // get query params from URL
 
176
  return (
177
  <section className="h-[100dvh] bg-neutral-950 flex flex-col">
178
  <Header tab={currentTab} onNewTab={setCurrentTab}>
179
+ <Button
180
+ variant="outline"
181
+ onClick={() => {
182
+ if (user?.id) router.push("/projects");
183
+ else openLoginWindow();
184
+ }}
185
+ >
186
+ <Import className="size-4 mr-1.5" />
187
+ Load Project
188
+ </Button>
189
  {project?._id ? (
190
  <SaveButton html={html} prompts={prompts} />
191
  ) : (
components/my-projects/index.tsx CHANGED
@@ -1,28 +1,43 @@
1
  "use client";
 
 
 
 
2
  import { useUser } from "@/hooks/useUser";
3
  import { Project } from "@/types";
4
  import { redirect } from "next/navigation";
5
  import { ProjectCard } from "./project-card";
6
- import { Plus } from "lucide-react";
7
- import Link from "next/link";
8
 
9
- export function MyProjects({ projects }: { projects: Project[] }) {
 
 
 
 
10
  const { user } = useUser();
11
  if (!user) {
12
  redirect("/");
13
  }
 
14
  return (
15
  <>
16
  <section className="max-w-[86rem] py-12 px-4 mx-auto">
17
- <div className="text-left">
18
- <h1 className="text-3xl font-bold text-white">
19
- <span className="capitalize">{user.fullname}</span>&apos;s DeepSite
20
- Projects
21
- </h1>
22
- <p className="text-muted-foreground text-base mt-1 max-w-xl">
23
- Create, manage, and explore your DeepSite projects.
24
- </p>
25
- </div>
 
 
 
 
 
 
 
26
  <div className="mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
27
  <Link
28
  href="/projects/new"
 
1
  "use client";
2
+ import { Plus } from "lucide-react";
3
+ import Link from "next/link";
4
+ import { useState } from "react";
5
+
6
  import { useUser } from "@/hooks/useUser";
7
  import { Project } from "@/types";
8
  import { redirect } from "next/navigation";
9
  import { ProjectCard } from "./project-card";
10
+ import { LoadProject } from "./load-project";
 
11
 
12
+ export function MyProjects({
13
+ projects: initialProjects,
14
+ }: {
15
+ projects: Project[];
16
+ }) {
17
  const { user } = useUser();
18
  if (!user) {
19
  redirect("/");
20
  }
21
+ const [projects, setProjects] = useState<Project[]>(initialProjects || []);
22
  return (
23
  <>
24
  <section className="max-w-[86rem] py-12 px-4 mx-auto">
25
+ <header className="flex items-center justify-between">
26
+ <div className="text-left">
27
+ <h1 className="text-3xl font-bold text-white">
28
+ <span className="capitalize">{user.fullname}</span>&apos;s
29
+ DeepSite Projects
30
+ </h1>
31
+ <p className="text-muted-foreground text-base mt-1 max-w-xl">
32
+ Create, manage, and explore your DeepSite projects.
33
+ </p>
34
+ </div>
35
+ <LoadProject
36
+ addProject={(project: Project) => {
37
+ setProjects((prev) => [...prev, project]);
38
+ }}
39
+ />
40
+ </header>
41
  <div className="mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
42
  <Link
43
  href="/projects/new"
components/my-projects/load-project.tsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Import } from "lucide-react";
3
+
4
+ import { Project } from "@/types";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from "@/components/ui/dialog";
12
+ import Loading from "@/components/loading";
13
+ import { Input } from "../ui/input";
14
+ import { toast } from "sonner";
15
+ import { api } from "@/lib/api";
16
+
17
+ export const LoadProject = ({
18
+ addProject,
19
+ }: {
20
+ addProject: (project: Project) => void;
21
+ }) => {
22
+ const [open, setOpen] = useState(false);
23
+ const [url, setUrl] = useState<string>("");
24
+ const [isLoading, setIsLoading] = useState(false);
25
+
26
+ const checkIfUrlIsValid = (url: string) => {
27
+ // should match a hugging face spaces URL like: https://huggingface.co/spaces/username/project or https://hf.co/spaces/username/project
28
+ const urlPattern = new RegExp(
29
+ /^(https?:\/\/)?(huggingface\.co|hf\.co)\/spaces\/([\w-]+)\/([\w-]+)$/,
30
+ "i"
31
+ );
32
+ return urlPattern.test(url);
33
+ };
34
+
35
+ const handleClick = async () => {
36
+ if (isLoading) return; // Prevent multiple clicks while loading
37
+ if (!url) {
38
+ toast.error("Please enter a URL.");
39
+ return;
40
+ }
41
+ if (!checkIfUrlIsValid(url)) {
42
+ toast.error("Please enter a valid Hugging Face Spaces URL.");
43
+ return;
44
+ }
45
+
46
+ const [username, namespace] = url
47
+ .replace("https://huggingface.co/spaces/", "")
48
+ .replace("https://hf.co/spaces/", "")
49
+ .split("/");
50
+
51
+ setIsLoading(true);
52
+ try {
53
+ const response = await api.post(`/me/projects/${username}/${namespace}`);
54
+ console.log("response", response);
55
+ toast.success("Project imported successfully!");
56
+ setOpen(false);
57
+ setUrl("");
58
+ addProject(response.data.project);
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ } catch (error: any) {
61
+ toast.error(
62
+ error?.response?.data?.error ?? "Failed to import the project."
63
+ );
64
+ } finally {
65
+ setIsLoading(false);
66
+ }
67
+ };
68
+
69
+ return (
70
+ <Dialog open={open} onOpenChange={setOpen}>
71
+ <DialogTrigger asChild>
72
+ <Button variant="outline">
73
+ <Import className="size-4 mr-1.5" />
74
+ Load existing Project
75
+ </Button>
76
+ </DialogTrigger>
77
+ <DialogContent className="sm:max-w-md !p-0 !rounded-3xl !bg-white !border-neutral-100 overflow-hidden text-center">
78
+ <DialogTitle className="hidden" />
79
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
80
+ <div className="flex items-center justify-center -space-x-4 mb-3">
81
+ <div className="size-11 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-2xl opacity-50">
82
+ 🎨
83
+ </div>
84
+ <div className="size-13 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-3xl z-2">
85
+ 🥳
86
+ </div>
87
+ <div className="size-11 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-2xl opacity-50">
88
+ 💎
89
+ </div>
90
+ </div>
91
+ <p className="text-2xl font-semibold text-neutral-950">
92
+ Import a Project
93
+ </p>
94
+ <p className="text-base text-neutral-500 mt-1.5">
95
+ Enter the URL of your Hugging Face Space to import an existing
96
+ project.
97
+ </p>
98
+ </header>
99
+ <main className="space-y-4 px-9 pb-9 pt-2">
100
+ <div>
101
+ <p className="text-sm text-neutral-700 mb-2">
102
+ Enter your Hugging Face Space
103
+ </p>
104
+ <Input
105
+ type="text"
106
+ placeholder="https://huggingface.com/spaces/username/project"
107
+ value={url}
108
+ onChange={(e) => setUrl(e.target.value)}
109
+ onBlur={(e) => {
110
+ const inputUrl = e.target.value.trim();
111
+ if (!inputUrl) {
112
+ setUrl("");
113
+ return;
114
+ }
115
+ if (!checkIfUrlIsValid(inputUrl)) {
116
+ toast.error("Please enter a valid URL.");
117
+ return;
118
+ }
119
+ setUrl(inputUrl);
120
+ }}
121
+ className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
122
+ />
123
+ </div>
124
+ <div>
125
+ <p className="text-sm text-neutral-700 mb-2">
126
+ Then, let&apos;s import it!
127
+ </p>
128
+ <Button
129
+ variant="black"
130
+ onClick={handleClick}
131
+ className="relative w-full"
132
+ >
133
+ {isLoading ? (
134
+ <>
135
+ <Loading
136
+ overlay={false}
137
+ className="ml-2 size-4 animate-spin"
138
+ />
139
+ Fetching your Space...
140
+ </>
141
+ ) : (
142
+ <>Import your Space</>
143
+ )}
144
+ </Button>
145
+ </div>
146
+ </main>
147
+ </DialogContent>
148
+ </Dialog>
149
+ );
150
+ };