Spaces:
Sleeping
Sleeping
[dyad] commited on
Commit ·
b9d5aef
1
Parent(s): a27839e
[dyad] Implement guest mode for builder - wrote 3 file(s), deleted 8 file(s)
Browse files- src/App.tsx +1 -9
- src/components/ProjectHistory.tsx +0 -117
- src/hooks/useAuth.ts +0 -56
- src/hooks/useProjects.ts +0 -253
- src/lib/supabaseClient.ts +0 -6
- src/pages/Auth.tsx +0 -245
- src/pages/Dashboard.tsx +0 -528
- src/pages/Index.tsx +49 -116
- src/pages/Landing.tsx +0 -288
- src/pages/VerifyEmail.tsx +0 -159
- src/types/project.ts +4 -18
src/App.tsx
CHANGED
|
@@ -6,10 +6,6 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
| 6 |
import { ThemeProvider } from "@/components/theme-provider";
|
| 7 |
import Index from "./pages/Index";
|
| 8 |
import NotFound from "./pages/NotFound";
|
| 9 |
-
import Landing from "./pages/Landing";
|
| 10 |
-
import Auth from "./pages/Auth";
|
| 11 |
-
import Dashboard from "./pages/Dashboard";
|
| 12 |
-
import VerifyEmail from "./pages/VerifyEmail";
|
| 13 |
|
| 14 |
const queryClient = new QueryClient();
|
| 15 |
|
|
@@ -21,11 +17,7 @@ const App = () => (
|
|
| 21 |
<Sonner />
|
| 22 |
<BrowserRouter>
|
| 23 |
<Routes>
|
| 24 |
-
<Route path="/" element={<
|
| 25 |
-
<Route path="/auth" element={<Auth />} />
|
| 26 |
-
<Route path="/verify-email" element={<VerifyEmail />} />
|
| 27 |
-
<Route path="/dashboard" element={<Dashboard />} />
|
| 28 |
-
<Route path="/builder" element={<Index />} />
|
| 29 |
<Route path="*" element={<NotFound />} />
|
| 30 |
</Routes>
|
| 31 |
</BrowserRouter>
|
|
|
|
| 6 |
import { ThemeProvider } from "@/components/theme-provider";
|
| 7 |
import Index from "./pages/Index";
|
| 8 |
import NotFound from "./pages/NotFound";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
const queryClient = new QueryClient();
|
| 11 |
|
|
|
|
| 17 |
<Sonner />
|
| 18 |
<BrowserRouter>
|
| 19 |
<Routes>
|
| 20 |
+
<Route path="/" element={<Index />} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
<Route path="*" element={<NotFound />} />
|
| 22 |
</Routes>
|
| 23 |
</BrowserRouter>
|
src/components/ProjectHistory.tsx
DELETED
|
@@ -1,117 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useState } from "react";
|
| 4 |
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
| 5 |
-
import { Badge } from "@/components/ui/badge";
|
| 6 |
-
import { Button } from "@/components/ui/button";
|
| 7 |
-
import {
|
| 8 |
-
CheckCircle,
|
| 9 |
-
Clock,
|
| 10 |
-
XCircle,
|
| 11 |
-
MoreHorizontal,
|
| 12 |
-
Play,
|
| 13 |
-
Eye
|
| 14 |
-
} from "lucide-react";
|
| 15 |
-
import { Project } from "@/types/project";
|
| 16 |
-
import { useNavigate } from "react-router-dom";
|
| 17 |
-
|
| 18 |
-
interface ProjectHistoryProps {
|
| 19 |
-
projects: Project[];
|
| 20 |
-
onViewProject: (project: Project) => void;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
const ProjectHistory = ({ projects, onViewProject }: ProjectHistoryProps) => {
|
| 24 |
-
const navigate = useNavigate();
|
| 25 |
-
const [expandedProject, setExpandedProject] = useState<string | null>(null);
|
| 26 |
-
|
| 27 |
-
const getStatusIcon = (status: Project["status"]) => {
|
| 28 |
-
switch (status) {
|
| 29 |
-
case 'completed':
|
| 30 |
-
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
| 31 |
-
case 'pending':
|
| 32 |
-
return <Clock className="h-4 w-4 text-blue-500" />;
|
| 33 |
-
case 'failed':
|
| 34 |
-
return <XCircle className="h-4 w-4 text-red-500" />;
|
| 35 |
-
default:
|
| 36 |
-
return <Clock className="h-4 w-4 text-gray-500" />;
|
| 37 |
-
}
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
const getStatusBadge = (status: Project["status"]) => {
|
| 41 |
-
switch (status) {
|
| 42 |
-
case 'completed':
|
| 43 |
-
return <Badge variant="secondary" className="bg-green-100 text-green-800">Completed</Badge>;
|
| 44 |
-
case 'pending':
|
| 45 |
-
return <Badge variant="secondary" className="bg-blue-100 text-blue-800">In Progress</Badge>;
|
| 46 |
-
case 'failed':
|
| 47 |
-
return <Badge variant="secondary" className="bg-red-100 text-red-800">Failed</Badge>;
|
| 48 |
-
default:
|
| 49 |
-
return <Badge variant="secondary" className="bg-gray-100 text-gray-800">Unknown</Badge>;
|
| 50 |
-
}
|
| 51 |
-
};
|
| 52 |
-
|
| 53 |
-
const formatDate = (date: Date) => {
|
| 54 |
-
return new Intl.DateTimeFormat("en-US", {
|
| 55 |
-
month: "short",
|
| 56 |
-
day: "numeric",
|
| 57 |
-
hour: "2-digit",
|
| 58 |
-
minute: "2-digit",
|
| 59 |
-
}).format(date);
|
| 60 |
-
};
|
| 61 |
-
|
| 62 |
-
return (
|
| 63 |
-
<div className="space-y-4">
|
| 64 |
-
{projects.map((project) => (
|
| 65 |
-
<Card key={project.id} className="border border-border">
|
| 66 |
-
<CardHeader className="pb-3">
|
| 67 |
-
<div className="flex justify-between items-start">
|
| 68 |
-
<div className="flex-1 min-w-0">
|
| 69 |
-
<CardTitle className="text-lg flex items-center gap-2">
|
| 70 |
-
<span className="truncate">{project.name}</span>
|
| 71 |
-
{getStatusIcon(project.status)}
|
| 72 |
-
</CardTitle>
|
| 73 |
-
<p className="text-sm text-muted-foreground mt-1">
|
| 74 |
-
Created {formatDate(project.createdAt)}
|
| 75 |
-
</p>
|
| 76 |
-
</div>
|
| 77 |
-
<div className="flex items-center gap-2 ml-4">
|
| 78 |
-
{getStatusBadge(project.status)}
|
| 79 |
-
<Button
|
| 80 |
-
variant="ghost"
|
| 81 |
-
size="icon"
|
| 82 |
-
onClick={() => setExpandedProject(expandedProject === project.id ? null : project.id)}
|
| 83 |
-
>
|
| 84 |
-
<MoreHorizontal className="h-4 w-4" />
|
| 85 |
-
</Button>
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
-
</CardHeader>
|
| 89 |
-
|
| 90 |
-
{expandedProject === project.id && (
|
| 91 |
-
<CardContent className="pt-0 border-t">
|
| 92 |
-
<div className="flex gap-2 mt-3">
|
| 93 |
-
<Button
|
| 94 |
-
size="sm"
|
| 95 |
-
onClick={() => navigate(`/builder?projectId=${project.id}`)}
|
| 96 |
-
>
|
| 97 |
-
<Play className="h-4 w-4 mr-2" />
|
| 98 |
-
Continue
|
| 99 |
-
</Button>
|
| 100 |
-
<Button
|
| 101 |
-
variant="outline"
|
| 102 |
-
size="sm"
|
| 103 |
-
onClick={() => onViewProject(project)}
|
| 104 |
-
>
|
| 105 |
-
<Eye className="h-4 w-4 mr-2" />
|
| 106 |
-
View
|
| 107 |
-
</Button>
|
| 108 |
-
</div>
|
| 109 |
-
</CardContent>
|
| 110 |
-
)}
|
| 111 |
-
</Card>
|
| 112 |
-
))}
|
| 113 |
-
</div>
|
| 114 |
-
);
|
| 115 |
-
};
|
| 116 |
-
|
| 117 |
-
export default ProjectHistory;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/hooks/useAuth.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
| 1 |
-
import { useState, useEffect } from 'react'
|
| 2 |
-
import { supabase } from '@/lib/supabaseClient'
|
| 3 |
-
import { User } from '@supabase/supabase-js'
|
| 4 |
-
|
| 5 |
-
export const useAuth = () => {
|
| 6 |
-
const [user, setUser] = useState<User | null>(null)
|
| 7 |
-
const [loading, setLoading] = useState(true)
|
| 8 |
-
|
| 9 |
-
useEffect(() => {
|
| 10 |
-
const getUser = async () => {
|
| 11 |
-
const { data: { user } } = await supabase.auth.getUser()
|
| 12 |
-
setUser(user)
|
| 13 |
-
setLoading(false)
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
getUser()
|
| 17 |
-
|
| 18 |
-
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
| 19 |
-
(_event, session) => {
|
| 20 |
-
setUser(session?.user || null)
|
| 21 |
-
setLoading(false)
|
| 22 |
-
}
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
return () => subscription.unsubscribe()
|
| 26 |
-
}, [])
|
| 27 |
-
|
| 28 |
-
const signIn = async (email: string, password: string) => {
|
| 29 |
-
const { data, error } = await supabase.auth.signInWithPassword({
|
| 30 |
-
email,
|
| 31 |
-
password,
|
| 32 |
-
})
|
| 33 |
-
return { data, error }
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
const signUp = async (email: string, password: string, firstName?: string, lastName?: string) => {
|
| 37 |
-
const { data, error } = await supabase.auth.signUp({
|
| 38 |
-
email,
|
| 39 |
-
password,
|
| 40 |
-
options: {
|
| 41 |
-
data: {
|
| 42 |
-
first_name: firstName,
|
| 43 |
-
last_name: lastName,
|
| 44 |
-
}
|
| 45 |
-
}
|
| 46 |
-
})
|
| 47 |
-
return { data, error }
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
const signOut = async () => {
|
| 51 |
-
const { error } = await supabase.auth.signOut()
|
| 52 |
-
return { error }
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
return { user, loading, signIn, signUp, signOut }
|
| 56 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/hooks/useProjects.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
| 1 |
-
import { useState, useEffect } from 'react';
|
| 2 |
-
import { supabase } from '@/lib/supabaseClient';
|
| 3 |
-
import { Project, ProjectVersion } from '@/types/project';
|
| 4 |
-
import { ChatMessage } from '@/services/gemini'
|
| 5 |
-
|
| 6 |
-
export const useProjects = (userId: string | null) => {
|
| 7 |
-
const [projects, setProjects] = useState<Project[]>([]);
|
| 8 |
-
const [versions, setVersions] = useState<ProjectVersion[]>([]);
|
| 9 |
-
const [loading, setLoading] = useState(true);
|
| 10 |
-
const [error, setError] = useState<string | null>(null);
|
| 11 |
-
|
| 12 |
-
useEffect(() => {
|
| 13 |
-
if (userId) {
|
| 14 |
-
fetchProjects();
|
| 15 |
-
} else {
|
| 16 |
-
setProjects([]);
|
| 17 |
-
setVersions([]);
|
| 18 |
-
setLoading(false);
|
| 19 |
-
}
|
| 20 |
-
}, [userId]);
|
| 21 |
-
|
| 22 |
-
const fetchProjects = async () => {
|
| 23 |
-
try {
|
| 24 |
-
setLoading(true);
|
| 25 |
-
const { data, error } = await supabase
|
| 26 |
-
.from('projects')
|
| 27 |
-
.select('*')
|
| 28 |
-
.eq('user_id', userId)
|
| 29 |
-
.order('updated_at', { ascending: false });
|
| 30 |
-
|
| 31 |
-
if (error) throw error;
|
| 32 |
-
|
| 33 |
-
setProjects(data.map(project => ({
|
| 34 |
-
id: project.id,
|
| 35 |
-
userId: project.user_id,
|
| 36 |
-
name: project.name,
|
| 37 |
-
status: project.status,
|
| 38 |
-
prompt: project.prompt,
|
| 39 |
-
code: project.code,
|
| 40 |
-
createdAt: new Date(project.created_at),
|
| 41 |
-
updatedAt: new Date(project.updated_at),
|
| 42 |
-
messages: project.messages || []
|
| 43 |
-
})));
|
| 44 |
-
} catch (err: any) {
|
| 45 |
-
setError(err.message);
|
| 46 |
-
} finally {
|
| 47 |
-
setLoading(false);
|
| 48 |
-
}
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
const fetchVersions = async (projectId: string) => {
|
| 52 |
-
if (!projectId) {
|
| 53 |
-
setVersions([]);
|
| 54 |
-
return;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
try {
|
| 58 |
-
const { data, error } = await supabase
|
| 59 |
-
.from('project_versions')
|
| 60 |
-
.select('*')
|
| 61 |
-
.eq('project_id', projectId)
|
| 62 |
-
.order('created_at', { ascending: false });
|
| 63 |
-
|
| 64 |
-
if (error) throw error;
|
| 65 |
-
|
| 66 |
-
setVersions(data.map(version => ({
|
| 67 |
-
id: version.id,
|
| 68 |
-
projectId: version.project_id,
|
| 69 |
-
code: version.code,
|
| 70 |
-
messages: version.messages || [],
|
| 71 |
-
createdAt: version.created_at
|
| 72 |
-
})));
|
| 73 |
-
} catch (err: any) {
|
| 74 |
-
console.error('Error fetching versions:', err);
|
| 75 |
-
}
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
const createProjectWithFirstVersion = async (projectData: Omit<Project, 'id' | 'userId' | 'createdAt' | 'updatedAt'>) => {
|
| 79 |
-
if (!userId) throw new Error('User not authenticated');
|
| 80 |
-
|
| 81 |
-
const { data, error } = await supabase
|
| 82 |
-
.from('projects')
|
| 83 |
-
.insert([
|
| 84 |
-
{
|
| 85 |
-
user_id: userId,
|
| 86 |
-
name: projectData.name,
|
| 87 |
-
status: projectData.status,
|
| 88 |
-
prompt: projectData.prompt,
|
| 89 |
-
code: projectData.code,
|
| 90 |
-
messages: projectData.messages
|
| 91 |
-
}
|
| 92 |
-
])
|
| 93 |
-
.select()
|
| 94 |
-
.single();
|
| 95 |
-
|
| 96 |
-
if (error) throw error;
|
| 97 |
-
|
| 98 |
-
return {
|
| 99 |
-
id: data.id,
|
| 100 |
-
userId: data.user_id,
|
| 101 |
-
name: data.name,
|
| 102 |
-
status: data.status,
|
| 103 |
-
prompt: data.prompt,
|
| 104 |
-
code: data.code,
|
| 105 |
-
createdAt: new Date(data.created_at),
|
| 106 |
-
updatedAt: new Date(data.updated_at),
|
| 107 |
-
messages: data.messages || []
|
| 108 |
-
};
|
| 109 |
-
};
|
| 110 |
-
|
| 111 |
-
const createEmptyProject = async (name: string) => {
|
| 112 |
-
if (!userId) throw new Error('User not authenticated');
|
| 113 |
-
|
| 114 |
-
const { data, error } = await supabase
|
| 115 |
-
.from('projects')
|
| 116 |
-
.insert([
|
| 117 |
-
{
|
| 118 |
-
user_id: userId,
|
| 119 |
-
name,
|
| 120 |
-
status: 'completed',
|
| 121 |
-
prompt: '',
|
| 122 |
-
code: '<!DOCTYPE html><html><head><title>Empty Project</title></head><body><h1>New Project</h1></body></html>',
|
| 123 |
-
messages: []
|
| 124 |
-
}
|
| 125 |
-
])
|
| 126 |
-
.select()
|
| 127 |
-
.single();
|
| 128 |
-
|
| 129 |
-
if (error) throw error;
|
| 130 |
-
|
| 131 |
-
await fetchProjects();
|
| 132 |
-
|
| 133 |
-
return {
|
| 134 |
-
id: data.id,
|
| 135 |
-
userId: data.user_id,
|
| 136 |
-
name: data.name,
|
| 137 |
-
status: data.status,
|
| 138 |
-
prompt: data.prompt,
|
| 139 |
-
code: data.code,
|
| 140 |
-
createdAt: new Date(data.created_at),
|
| 141 |
-
updatedAt: new Date(data.updated_at),
|
| 142 |
-
messages: data.messages || []
|
| 143 |
-
};
|
| 144 |
-
};
|
| 145 |
-
|
| 146 |
-
const createNewVersion = async (projectId: string, versionData: { code: string; messages: ChatMessage[] }) => {
|
| 147 |
-
const { data, error } = await supabase
|
| 148 |
-
.from('project_versions')
|
| 149 |
-
.insert([
|
| 150 |
-
{
|
| 151 |
-
project_id: projectId,
|
| 152 |
-
code: versionData.code,
|
| 153 |
-
messages: versionData.messages
|
| 154 |
-
}
|
| 155 |
-
])
|
| 156 |
-
.select()
|
| 157 |
-
.single();
|
| 158 |
-
|
| 159 |
-
if (error) throw error;
|
| 160 |
-
|
| 161 |
-
// Update the project with the new code and messages
|
| 162 |
-
const { data: projectData, error: projectError } = await supabase
|
| 163 |
-
.from('projects')
|
| 164 |
-
.update({
|
| 165 |
-
code: versionData.code,
|
| 166 |
-
messages: versionData.messages,
|
| 167 |
-
updated_at: new Date().toISOString()
|
| 168 |
-
})
|
| 169 |
-
.eq('id', projectId)
|
| 170 |
-
.select()
|
| 171 |
-
.single();
|
| 172 |
-
|
| 173 |
-
if (projectError) throw projectError;
|
| 174 |
-
|
| 175 |
-
await fetchProjects();
|
| 176 |
-
await fetchVersions(projectId);
|
| 177 |
-
|
| 178 |
-
return {
|
| 179 |
-
id: projectData.id,
|
| 180 |
-
userId: projectData.user_id,
|
| 181 |
-
name: projectData.name,
|
| 182 |
-
status: projectData.status,
|
| 183 |
-
prompt: projectData.prompt,
|
| 184 |
-
code: projectData.code,
|
| 185 |
-
createdAt: new Date(projectData.created_at),
|
| 186 |
-
updatedAt: new Date(projectData.updated_at),
|
| 187 |
-
messages: projectData.messages || []
|
| 188 |
-
};
|
| 189 |
-
};
|
| 190 |
-
|
| 191 |
-
const restoreVersion = async (projectId: string, versionId: string) => {
|
| 192 |
-
const { data, error } = await supabase
|
| 193 |
-
.from('project_versions')
|
| 194 |
-
.select('code, messages')
|
| 195 |
-
.eq('id', versionId)
|
| 196 |
-
.single();
|
| 197 |
-
|
| 198 |
-
if (error) throw error;
|
| 199 |
-
|
| 200 |
-
const { data: projectData, error: projectError } = await supabase
|
| 201 |
-
.from('projects')
|
| 202 |
-
.update({
|
| 203 |
-
code: data.code,
|
| 204 |
-
messages: data.messages,
|
| 205 |
-
updated_at: new Date().toISOString()
|
| 206 |
-
})
|
| 207 |
-
.eq('id', projectId)
|
| 208 |
-
.select()
|
| 209 |
-
.single();
|
| 210 |
-
|
| 211 |
-
if (projectError) throw projectError;
|
| 212 |
-
|
| 213 |
-
await fetchProjects();
|
| 214 |
-
await fetchVersions(projectId);
|
| 215 |
-
|
| 216 |
-
return {
|
| 217 |
-
id: projectData.id,
|
| 218 |
-
userId: projectData.user_id,
|
| 219 |
-
name: projectData.name,
|
| 220 |
-
status: projectData.status,
|
| 221 |
-
prompt: projectData.prompt,
|
| 222 |
-
code: projectData.code,
|
| 223 |
-
createdAt: new Date(projectData.created_at),
|
| 224 |
-
updatedAt: new Date(projectData.updated_at),
|
| 225 |
-
messages: projectData.messages || []
|
| 226 |
-
};
|
| 227 |
-
};
|
| 228 |
-
|
| 229 |
-
const deleteProject = async (projectId: string) => {
|
| 230 |
-
const { error } = await supabase
|
| 231 |
-
.from('projects')
|
| 232 |
-
.delete()
|
| 233 |
-
.eq('id', projectId);
|
| 234 |
-
|
| 235 |
-
if (error) throw error;
|
| 236 |
-
|
| 237 |
-
await fetchProjects();
|
| 238 |
-
};
|
| 239 |
-
|
| 240 |
-
return {
|
| 241 |
-
projects,
|
| 242 |
-
versions,
|
| 243 |
-
loading,
|
| 244 |
-
error,
|
| 245 |
-
fetchProjects,
|
| 246 |
-
fetchVersions,
|
| 247 |
-
createProjectWithFirstVersion,
|
| 248 |
-
createEmptyProject,
|
| 249 |
-
createNewVersion,
|
| 250 |
-
restoreVersion,
|
| 251 |
-
deleteProject
|
| 252 |
-
};
|
| 253 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/supabaseClient.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
import { createClient } from '@supabase/supabase-js'
|
| 2 |
-
|
| 3 |
-
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
| 4 |
-
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
| 5 |
-
|
| 6 |
-
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pages/Auth.tsx
DELETED
|
@@ -1,245 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { useState } from "react"
|
| 4 |
-
import { Button } from "@/components/ui/button"
|
| 5 |
-
import { Input } from "@/components/ui/input"
|
| 6 |
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
| 7 |
-
import { useNavigate } from "react-router-dom"
|
| 8 |
-
import { useAuth } from "@/hooks/useAuth"
|
| 9 |
-
import { Sparkles, Mail, Lock, User, ArrowLeft } from "lucide-react"
|
| 10 |
-
import { motion, AnimatePresence } from "framer-motion"
|
| 11 |
-
import AnimatedBackground from "@/components/AnimatedBackground"
|
| 12 |
-
import { Badge } from "@/components/ui/badge"
|
| 13 |
-
|
| 14 |
-
const Auth = () => {
|
| 15 |
-
const [isSignUp, setIsSignUp] = useState(false)
|
| 16 |
-
const [email, setEmail] = useState("")
|
| 17 |
-
const [password, setPassword] = useState("")
|
| 18 |
-
const [firstName, setFirstName] = useState("")
|
| 19 |
-
const [lastName, setLastName] = useState("")
|
| 20 |
-
const [loading, setLoading] = useState(false)
|
| 21 |
-
const [error, setError] = useState("")
|
| 22 |
-
const navigate = useNavigate()
|
| 23 |
-
const { signIn, signUp } = useAuth()
|
| 24 |
-
|
| 25 |
-
const handleSubmit = async (e: React.FormEvent) => {
|
| 26 |
-
e.preventDefault()
|
| 27 |
-
setLoading(true)
|
| 28 |
-
setError("")
|
| 29 |
-
|
| 30 |
-
try {
|
| 31 |
-
if (isSignUp) {
|
| 32 |
-
const { data, error } = await signUp(email, password, firstName, lastName)
|
| 33 |
-
if (error) throw error
|
| 34 |
-
|
| 35 |
-
// If sign up successful but email not confirmed, redirect to verification page
|
| 36 |
-
if (data.user && !data.user.email_confirmed_at) {
|
| 37 |
-
navigate('/verify-email')
|
| 38 |
-
} else if (data.user) {
|
| 39 |
-
// If email already confirmed, go to dashboard
|
| 40 |
-
navigate('/dashboard')
|
| 41 |
-
}
|
| 42 |
-
} else {
|
| 43 |
-
const { error } = await signIn(email, password)
|
| 44 |
-
if (error) throw error
|
| 45 |
-
navigate('/dashboard')
|
| 46 |
-
}
|
| 47 |
-
} catch (err: any) {
|
| 48 |
-
setError(err.message || "An error occurred")
|
| 49 |
-
} finally {
|
| 50 |
-
setLoading(false)
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
return (
|
| 55 |
-
<div className="min-h-screen w-full flex items-center justify-center p-4 relative overflow-hidden">
|
| 56 |
-
<AnimatedBackground />
|
| 57 |
-
|
| 58 |
-
<motion.div
|
| 59 |
-
initial={{ opacity: 0, y: 20 }}
|
| 60 |
-
animate={{ opacity: 1, y: 0 }}
|
| 61 |
-
transition={{ duration: 0.5 }}
|
| 62 |
-
className="w-full max-w-md z-10"
|
| 63 |
-
>
|
| 64 |
-
<Card className="shadow-2xl backdrop-blur-xl border border-gray-800 bg-gray-900/80">
|
| 65 |
-
<CardHeader className="text-center pb-6">
|
| 66 |
-
<motion.div
|
| 67 |
-
className="flex justify-center mb-4"
|
| 68 |
-
whileHover={{ scale: 1.1 }}
|
| 69 |
-
whileTap={{ scale: 0.9 }}
|
| 70 |
-
>
|
| 71 |
-
<div className="w-16 h-16 flex items-center justify-center">
|
| 72 |
-
<img src="/front-pilot.svg" alt="FrontPilot Logo" className="w-16 h-16" />
|
| 73 |
-
</div>
|
| 74 |
-
</motion.div>
|
| 75 |
-
<div className="flex items-center justify-center">
|
| 76 |
-
<CardTitle className="text-3xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400">
|
| 77 |
-
{isSignUp ? "Create Account" : "Welcome Back"}
|
| 78 |
-
</CardTitle>
|
| 79 |
-
<span className="ml-3 text-[0.6rem] bg-yellow-500/20 text-yellow-500 font-bold px-1.5 py-0.5 rounded-full backdrop-blur-sm border border-yellow-500/30">BETA</span>
|
| 80 |
-
</div>
|
| 81 |
-
<CardDescription className="text-base text-gray-400">
|
| 82 |
-
{isSignUp
|
| 83 |
-
? "Join us to start building amazing websites."
|
| 84 |
-
: "Sign in to continue to your dashboard."}
|
| 85 |
-
</CardDescription>
|
| 86 |
-
</CardHeader>
|
| 87 |
-
|
| 88 |
-
<form onSubmit={handleSubmit}>
|
| 89 |
-
<CardContent className="space-y-5">
|
| 90 |
-
{error && (
|
| 91 |
-
<motion.div
|
| 92 |
-
initial={{ opacity: 0, x: -20 }}
|
| 93 |
-
animate={{ opacity: 1, x: 0 }}
|
| 94 |
-
className="text-red-400 text-sm p-3 bg-red-500/10 rounded-lg border border-red-500/20"
|
| 95 |
-
>
|
| 96 |
-
{error}
|
| 97 |
-
</motion.div>
|
| 98 |
-
)}
|
| 99 |
-
|
| 100 |
-
<AnimatePresence mode="wait">
|
| 101 |
-
{isSignUp && (
|
| 102 |
-
<motion.div
|
| 103 |
-
initial={{ opacity: 0, height: 0 }}
|
| 104 |
-
animate={{ opacity: 1, height: "auto" }}
|
| 105 |
-
exit={{ opacity: 0, height: 0 }}
|
| 106 |
-
transition={{ duration: 0.3 }}
|
| 107 |
-
className="grid grid-cols-2 gap-4"
|
| 108 |
-
>
|
| 109 |
-
<div className="space-y-2">
|
| 110 |
-
<label htmlFor="firstName" className="text-sm font-medium text-gray-300 flex items-center">
|
| 111 |
-
<User className="h-4 w-4 mr-2" />
|
| 112 |
-
First Name
|
| 113 |
-
</label>
|
| 114 |
-
<Input
|
| 115 |
-
id="firstName"
|
| 116 |
-
type="text"
|
| 117 |
-
placeholder="John"
|
| 118 |
-
value={firstName}
|
| 119 |
-
onChange={(e) => setFirstName(e.target.value)}
|
| 120 |
-
required={isSignUp}
|
| 121 |
-
className="bg-gray-800/50 border-gray-700 text-white focus:ring-2 focus:ring-blue-500/50"
|
| 122 |
-
/>
|
| 123 |
-
</div>
|
| 124 |
-
<div className="space-y-2">
|
| 125 |
-
<label htmlFor="lastName" className="text-sm font-medium text-gray-300 flex items-center">
|
| 126 |
-
<User className="h-4 w-4 mr-2" />
|
| 127 |
-
Last Name
|
| 128 |
-
</label>
|
| 129 |
-
<Input
|
| 130 |
-
id="lastName"
|
| 131 |
-
type="text"
|
| 132 |
-
placeholder="Doe"
|
| 133 |
-
value={lastName}
|
| 134 |
-
onChange={(e) => setLastName(e.target.value)}
|
| 135 |
-
required={isSignUp}
|
| 136 |
-
className="bg-gray-800/50 border-gray-700 text-white focus:ring-2 focus:ring-blue-500/50"
|
| 137 |
-
/>
|
| 138 |
-
</div>
|
| 139 |
-
</motion.div>
|
| 140 |
-
)}
|
| 141 |
-
</AnimatePresence>
|
| 142 |
-
|
| 143 |
-
<div className="space-y-2">
|
| 144 |
-
<label htmlFor="email" className="text-sm font-medium text-gray-300 flex items-center">
|
| 145 |
-
<Mail className="h-4 w-4 mr-2" />
|
| 146 |
-
Email
|
| 147 |
-
</label>
|
| 148 |
-
<Input
|
| 149 |
-
id="email"
|
| 150 |
-
type="email"
|
| 151 |
-
placeholder="name@company.com"
|
| 152 |
-
value={email}
|
| 153 |
-
onChange={(e) => setEmail(e.target.value)}
|
| 154 |
-
required
|
| 155 |
-
className="bg-gray-800/50 border-gray-700 text-white focus:ring-2 focus:ring-blue-500/50"
|
| 156 |
-
/>
|
| 157 |
-
</div>
|
| 158 |
-
|
| 159 |
-
<div className="space-y-2">
|
| 160 |
-
<label htmlFor="password" className="text-sm font-medium text-gray-300 flex items-center">
|
| 161 |
-
<Lock className="h-4 w-4 mr-2" />
|
| 162 |
-
Password
|
| 163 |
-
</label>
|
| 164 |
-
<Input
|
| 165 |
-
id="password"
|
| 166 |
-
type="password"
|
| 167 |
-
placeholder="••••••••"
|
| 168 |
-
value={password}
|
| 169 |
-
onChange={(e) => setPassword(e.target.value)}
|
| 170 |
-
required
|
| 171 |
-
className="bg-gray-800/50 border-gray-700 text-white focus:ring-2 focus:ring-blue-500/50"
|
| 172 |
-
/>
|
| 173 |
-
</div>
|
| 174 |
-
</CardContent>
|
| 175 |
-
|
| 176 |
-
<CardFooter className="flex flex-col pt-4">
|
| 177 |
-
<motion.div
|
| 178 |
-
whileHover={{ scale: 1.02 }}
|
| 179 |
-
whileTap={{ scale: 0.98 }}
|
| 180 |
-
className="w-full"
|
| 181 |
-
>
|
| 182 |
-
<Button
|
| 183 |
-
type="submit"
|
| 184 |
-
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-600 py-6 text-lg shadow-lg hover:shadow-xl"
|
| 185 |
-
disabled={loading}
|
| 186 |
-
>
|
| 187 |
-
{loading ? (
|
| 188 |
-
<motion.span
|
| 189 |
-
initial={{ opacity: 0 }}
|
| 190 |
-
animate={{ opacity: 1 }}
|
| 191 |
-
className="flex items-center"
|
| 192 |
-
>
|
| 193 |
-
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 194 |
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
| 195 |
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 196 |
-
</svg>
|
| 197 |
-
Processing...
|
| 198 |
-
</motion.span>
|
| 199 |
-
) : (
|
| 200 |
-
isSignUp ? "Create Account" : "Sign In"
|
| 201 |
-
)}
|
| 202 |
-
</Button>
|
| 203 |
-
</motion.div>
|
| 204 |
-
|
| 205 |
-
<motion.div
|
| 206 |
-
whileHover={{ scale: 1.02 }}
|
| 207 |
-
whileTap={{ scale: 0.98 }}
|
| 208 |
-
className="mt-4 w-full"
|
| 209 |
-
>
|
| 210 |
-
<Button
|
| 211 |
-
type="button"
|
| 212 |
-
variant="ghost"
|
| 213 |
-
onClick={() => setIsSignUp(!isSignUp)}
|
| 214 |
-
className="w-full text-gray-300 hover:text-white hover:bg-gray-800/50 py-5"
|
| 215 |
-
>
|
| 216 |
-
{isSignUp
|
| 217 |
-
? "Already have an account? Sign In"
|
| 218 |
-
: "Don't have an account? Sign Up"}
|
| 219 |
-
</Button>
|
| 220 |
-
</motion.div>
|
| 221 |
-
|
| 222 |
-
<motion.div
|
| 223 |
-
whileHover={{ scale: 1.05 }}
|
| 224 |
-
whileTap={{ scale: 0.9 }}
|
| 225 |
-
className="mt-4 w-full"
|
| 226 |
-
>
|
| 227 |
-
<Button
|
| 228 |
-
type="button"
|
| 229 |
-
variant="outline"
|
| 230 |
-
onClick={() => navigate('/')}
|
| 231 |
-
className="w-full text-gray-300 hover:text-white hover:bg-gray-800/50 py-5"
|
| 232 |
-
>
|
| 233 |
-
<ArrowLeft className="h-4 w-4 mr-2" />
|
| 234 |
-
Back to Home
|
| 235 |
-
</Button>
|
| 236 |
-
</motion.div>
|
| 237 |
-
</CardFooter>
|
| 238 |
-
</form>
|
| 239 |
-
</Card>
|
| 240 |
-
</motion.div>
|
| 241 |
-
</div>
|
| 242 |
-
)
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
export default Auth
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pages/Dashboard.tsx
DELETED
|
@@ -1,528 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect } from "react"
|
| 4 |
-
import { Button } from "@/components/ui/button"
|
| 5 |
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
| 6 |
-
import { Input } from "@/components/ui/input"
|
| 7 |
-
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
| 8 |
-
import { useNavigate } from "react-router-dom"
|
| 9 |
-
import { useAuth } from "@/hooks/useAuth"
|
| 10 |
-
import { useProjects } from "@/hooks/useProjects"
|
| 11 |
-
import { supabase } from "@/lib/supabaseClient"
|
| 12 |
-
import { Plus, FileText, Calendar, User, Loader2, LayoutDashboard, LogOut, Search, Grid3X3, List, Trash2, MoreHorizontal } from "lucide-react"
|
| 13 |
-
import { Project } from "@/types/project"
|
| 14 |
-
import { ModeToggle } from "@/components/theme-toggle"
|
| 15 |
-
import { motion, AnimatePresence } from "framer-motion"
|
| 16 |
-
import { Badge } from "@/components/ui/badge"
|
| 17 |
-
import {
|
| 18 |
-
DropdownMenu,
|
| 19 |
-
DropdownMenuContent,
|
| 20 |
-
DropdownMenuItem,
|
| 21 |
-
DropdownMenuTrigger,
|
| 22 |
-
} from "@/components/ui/dropdown-menu"
|
| 23 |
-
import { useToast } from "@/hooks/use-toast"
|
| 24 |
-
|
| 25 |
-
const Dashboard = () => {
|
| 26 |
-
const navigate = useNavigate()
|
| 27 |
-
const { user, signOut } = useAuth()
|
| 28 |
-
const { projects, loading, error, createEmptyProject, deleteProject } = useProjects(user?.id || null)
|
| 29 |
-
const [userProfile, setUserProfile] = useState<{ first_name: string; last_name: string } | null>(null)
|
| 30 |
-
const [profileLoading, setProfileLoading] = useState(true)
|
| 31 |
-
const [showNameDialog, setShowNameDialog] = useState(false)
|
| 32 |
-
const [prototypeName, setPrototypeName] = useState("")
|
| 33 |
-
const [searchTerm, setSearchTerm] = useState("")
|
| 34 |
-
const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
|
| 35 |
-
const [projectToDelete, setProjectToDelete] = useState<string | null>(null)
|
| 36 |
-
const { toast } = useToast()
|
| 37 |
-
|
| 38 |
-
useEffect(() => {
|
| 39 |
-
if (user) {
|
| 40 |
-
fetchUserProfile()
|
| 41 |
-
}
|
| 42 |
-
}, [user])
|
| 43 |
-
|
| 44 |
-
const fetchUserProfile = async () => {
|
| 45 |
-
try {
|
| 46 |
-
const { data, error } = await supabase
|
| 47 |
-
.from('profiles')
|
| 48 |
-
.select('first_name, last_name')
|
| 49 |
-
.eq('id', user?.id)
|
| 50 |
-
.single()
|
| 51 |
-
|
| 52 |
-
if (error) throw error
|
| 53 |
-
setUserProfile(data)
|
| 54 |
-
} catch (err) {
|
| 55 |
-
console.error('Error fetching user profile:', err)
|
| 56 |
-
} finally {
|
| 57 |
-
setProfileLoading(false)
|
| 58 |
-
}
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
const handleCreatePrototype = () => {
|
| 62 |
-
setShowNameDialog(true)
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
const handleNameSubmit = async () => {
|
| 66 |
-
if (!prototypeName.trim()) return;
|
| 67 |
-
|
| 68 |
-
try {
|
| 69 |
-
const newProject = await createEmptyProject(prototypeName);
|
| 70 |
-
setShowNameDialog(false);
|
| 71 |
-
setPrototypeName("");
|
| 72 |
-
navigate(`/builder?projectId=${newProject.id}`);
|
| 73 |
-
} catch (err) {
|
| 74 |
-
console.error('Error creating prototype:', err);
|
| 75 |
-
toast({
|
| 76 |
-
title: "Error",
|
| 77 |
-
description: "Failed to create prototype. Please try again.",
|
| 78 |
-
variant: "destructive",
|
| 79 |
-
});
|
| 80 |
-
}
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
const handleSignOut = async () => {
|
| 84 |
-
await signOut()
|
| 85 |
-
navigate('/')
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
const formatDate = (date: Date) => {
|
| 89 |
-
return new Intl.DateTimeFormat('en-US', {
|
| 90 |
-
month: 'short',
|
| 91 |
-
day: 'numeric',
|
| 92 |
-
year: 'numeric'
|
| 93 |
-
}).format(date)
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
const handleDeleteProject = async (projectId: string) => {
|
| 97 |
-
try {
|
| 98 |
-
await deleteProject(projectId);
|
| 99 |
-
toast({
|
| 100 |
-
title: "Project deleted",
|
| 101 |
-
description: "Your prototype has been successfully deleted.",
|
| 102 |
-
});
|
| 103 |
-
} catch (err) {
|
| 104 |
-
console.error('Error deleting project:', err);
|
| 105 |
-
toast({
|
| 106 |
-
title: "Error",
|
| 107 |
-
description: "Failed to delete project. Please try again.",
|
| 108 |
-
variant: "destructive",
|
| 109 |
-
});
|
| 110 |
-
} finally {
|
| 111 |
-
setProjectToDelete(null);
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
const filteredProjects = projects.filter(project =>
|
| 116 |
-
project.name.toLowerCase().includes(searchTerm.toLowerCase())
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
if (!user) {
|
| 120 |
-
return (
|
| 121 |
-
<div className="min-h-screen flex items-center justify-center bg-secondary/50">
|
| 122 |
-
<div className="text-center">
|
| 123 |
-
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
|
| 124 |
-
<p className="text-muted-foreground">Loading...</p>
|
| 125 |
-
</div>
|
| 126 |
-
</div>
|
| 127 |
-
)
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
return (
|
| 131 |
-
<div className="min-h-screen flex bg-background">
|
| 132 |
-
{/* Sidebar */}
|
| 133 |
-
<motion.aside
|
| 134 |
-
initial={{ x: -300 }}
|
| 135 |
-
animate={{ x: 0 }}
|
| 136 |
-
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
| 137 |
-
className="w-64 flex-shrink-0 bg-card border-r flex flex-col shadow-xl"
|
| 138 |
-
>
|
| 139 |
-
<div className="h-16 border-b flex items-center px-6">
|
| 140 |
-
<motion.div
|
| 141 |
-
className="flex items-center space-x-3"
|
| 142 |
-
whileHover={{ scale: 1.02 }}
|
| 143 |
-
>
|
| 144 |
-
<div className="w-10 h-10 flex items-center justify-center">
|
| 145 |
-
<img src="/front-pilot.svg" alt="FrontPilot Logo" className="w-10 h-10" />
|
| 146 |
-
</div>
|
| 147 |
-
<div className="flex items-center">
|
| 148 |
-
<span className="font-bold text-xl bg-clip-text text-transparent bg-gradient-to-r from-primary to-indigo-500">
|
| 149 |
-
FrontPilot
|
| 150 |
-
</span>
|
| 151 |
-
<span className="ml-2 text-[0.6rem] bg-yellow-500/20 text-yellow-500 font-bold px-1.5 py-0.5 rounded-full backdrop-blur-sm border border-yellow-500/30">BETA</span>
|
| 152 |
-
</div>
|
| 153 |
-
</motion.div>
|
| 154 |
-
</div>
|
| 155 |
-
|
| 156 |
-
<nav className="flex-1 px-4 py-6 space-y-2">
|
| 157 |
-
<motion.div
|
| 158 |
-
whileHover={{ x: 5 }}
|
| 159 |
-
whileTap={{ scale: 0.98 }}
|
| 160 |
-
>
|
| 161 |
-
<Button variant="secondary" className="w-full justify-start py-6 text-base">
|
| 162 |
-
<LayoutDashboard className="mr-3 h-5 w-5" />
|
| 163 |
-
Dashboard
|
| 164 |
-
</Button>
|
| 165 |
-
</motion.div>
|
| 166 |
-
|
| 167 |
-
<motion.div
|
| 168 |
-
whileHover={{ x: 5 }}
|
| 169 |
-
whileTap={{ scale: 0.98 }}
|
| 170 |
-
>
|
| 171 |
-
<Button
|
| 172 |
-
variant="ghost"
|
| 173 |
-
className="w-full justify-start py-6 text-base"
|
| 174 |
-
onClick={handleCreatePrototype}
|
| 175 |
-
>
|
| 176 |
-
<Plus className="mr-3 h-5 w-5" />
|
| 177 |
-
New Prototype
|
| 178 |
-
</Button>
|
| 179 |
-
</motion.div>
|
| 180 |
-
</nav>
|
| 181 |
-
|
| 182 |
-
<div className="mt-auto p-4 border-t">
|
| 183 |
-
<div className="flex items-center mb-4">
|
| 184 |
-
<div className="w-12 h-12 rounded-xl bg-secondary flex items-center justify-center mr-3 shadow-md">
|
| 185 |
-
<User className="h-6 w-6 text-muted-foreground" />
|
| 186 |
-
</div>
|
| 187 |
-
<div>
|
| 188 |
-
<p className="text-sm font-medium text-foreground">
|
| 189 |
-
{profileLoading ? "Loading..." : userProfile ? `${userProfile.first_name} ${userProfile.last_name}` : user.email?.split('@')[0]}
|
| 190 |
-
</p>
|
| 191 |
-
<p className="text-xs text-muted-foreground truncate max-w-[120px]">
|
| 192 |
-
{user.email}
|
| 193 |
-
</p>
|
| 194 |
-
</div>
|
| 195 |
-
</div>
|
| 196 |
-
|
| 197 |
-
<motion.div
|
| 198 |
-
whileHover={{ scale: 1.02 }}
|
| 199 |
-
whileTap={{ scale: 0.98 }}
|
| 200 |
-
>
|
| 201 |
-
<Button
|
| 202 |
-
variant="outline"
|
| 203 |
-
className="w-full py-5"
|
| 204 |
-
onClick={handleSignOut}
|
| 205 |
-
>
|
| 206 |
-
<LogOut className="mr-2 h-4 w-4" />
|
| 207 |
-
Sign Out
|
| 208 |
-
</Button>
|
| 209 |
-
</motion.div>
|
| 210 |
-
</div>
|
| 211 |
-
</motion.aside>
|
| 212 |
-
|
| 213 |
-
{/* Main Content */}
|
| 214 |
-
<main className="flex-1 overflow-hidden">
|
| 215 |
-
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur-xl">
|
| 216 |
-
<div className="container mx-auto px-6 py-4 flex items-center justify-between">
|
| 217 |
-
<div>
|
| 218 |
-
<h1 className="text-2xl font-bold text-foreground">My Prototypes</h1>
|
| 219 |
-
<p className="text-sm text-muted-foreground">Manage and create your web projects</p>
|
| 220 |
-
</div>
|
| 221 |
-
|
| 222 |
-
<div className="flex items-center space-x-4">
|
| 223 |
-
<div className="relative">
|
| 224 |
-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
| 225 |
-
<Input
|
| 226 |
-
type="text"
|
| 227 |
-
placeholder="Search prototypes..."
|
| 228 |
-
className="pl-10 pr-4 py-2 w-64"
|
| 229 |
-
value={searchTerm}
|
| 230 |
-
onChange={(e) => setSearchTerm(e.target.value)}
|
| 231 |
-
/>
|
| 232 |
-
</div>
|
| 233 |
-
|
| 234 |
-
<div className="flex items-center space-x-2 bg-secondary rounded-lg p-1">
|
| 235 |
-
<Button
|
| 236 |
-
variant={viewMode === "grid" ? "secondary" : "ghost"}
|
| 237 |
-
size="icon"
|
| 238 |
-
className="h-9 w-9"
|
| 239 |
-
onClick={() => setViewMode("grid")}
|
| 240 |
-
>
|
| 241 |
-
<Grid3X3 className="h-4 w-4" />
|
| 242 |
-
</Button>
|
| 243 |
-
<Button
|
| 244 |
-
variant={viewMode === "list" ? "secondary" : "ghost"}
|
| 245 |
-
size="icon"
|
| 246 |
-
className="h-9 w-9"
|
| 247 |
-
onClick={() => setViewMode("list")}
|
| 248 |
-
>
|
| 249 |
-
<List className="h-4 w-4" />
|
| 250 |
-
</Button>
|
| 251 |
-
</div>
|
| 252 |
-
|
| 253 |
-
<ModeToggle />
|
| 254 |
-
</div>
|
| 255 |
-
</div>
|
| 256 |
-
</header>
|
| 257 |
-
|
| 258 |
-
<div className="container mx-auto px-6 py-8">
|
| 259 |
-
{loading ? (
|
| 260 |
-
<div className="flex justify-center items-center h-96">
|
| 261 |
-
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
| 262 |
-
</div>
|
| 263 |
-
) : error ? (
|
| 264 |
-
<motion.div
|
| 265 |
-
initial={{ opacity: 0, y: 20 }}
|
| 266 |
-
animate={{ opacity: 1, y: 0 }}
|
| 267 |
-
className="text-center py-20"
|
| 268 |
-
>
|
| 269 |
-
<div className="mx-auto w-24 h-24 bg-red-500/10 rounded-full flex items-center justify-center mb-6">
|
| 270 |
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 271 |
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
| 272 |
-
</svg>
|
| 273 |
-
</div>
|
| 274 |
-
<h3 className="text-3xl font-bold text-foreground mb-4">Error Loading Prototypes</h3>
|
| 275 |
-
<p className="text-muted-foreground mb-8 max-w-md mx-auto">{error}</p>
|
| 276 |
-
<Button
|
| 277 |
-
onClick={() => window.location.reload()}
|
| 278 |
-
className="bg-primary text-primary-foreground hover:bg-primary/90 py-5 px-8 text-lg"
|
| 279 |
-
>
|
| 280 |
-
Retry
|
| 281 |
-
</Button>
|
| 282 |
-
</motion.div>
|
| 283 |
-
) : filteredProjects.length === 0 ? (
|
| 284 |
-
searchTerm ? (
|
| 285 |
-
<motion.div
|
| 286 |
-
initial={{ opacity: 0 }}
|
| 287 |
-
animate={{ opacity: 1 }}
|
| 288 |
-
className="text-center py-20"
|
| 289 |
-
>
|
| 290 |
-
<div className="mx-auto w-24 h-24 bg-secondary rounded-full flex items-center justify-center mb-6">
|
| 291 |
-
<Search className="h-12 w-12 text-muted-foreground" />
|
| 292 |
-
</div>
|
| 293 |
-
<h3 className="text-2xl font-bold text-foreground mb-2">No prototypes found</h3>
|
| 294 |
-
<p className="text-muted-foreground mb-6">
|
| 295 |
-
No prototypes match your search for "{searchTerm}"
|
| 296 |
-
</p>
|
| 297 |
-
<Button
|
| 298 |
-
onClick={() => setSearchTerm("")}
|
| 299 |
-
variant="outline"
|
| 300 |
-
className="py-5 px-6"
|
| 301 |
-
>
|
| 302 |
-
Clear Search
|
| 303 |
-
</Button>
|
| 304 |
-
</motion.div>
|
| 305 |
-
) : (
|
| 306 |
-
<motion.div
|
| 307 |
-
initial={{ opacity: 0, y: 20 }}
|
| 308 |
-
animate={{ opacity: 1, y: 0 }}
|
| 309 |
-
transition={{ duration: 0.5 }}
|
| 310 |
-
className="text-center py-20 border-2 border-dashed rounded-2xl"
|
| 311 |
-
>
|
| 312 |
-
<div className="mx-auto w-24 h-24 bg-secondary rounded-full flex items-center justify-center mb-6">
|
| 313 |
-
<FileText className="h-12 w-12 text-muted-foreground" />
|
| 314 |
-
</div>
|
| 315 |
-
<h3 className="text-3xl font-bold text-foreground mb-4">No prototypes yet</h3>
|
| 316 |
-
<p className="text-muted-foreground mb-8 max-w-md mx-auto">
|
| 317 |
-
Get started by creating your first prototype. Describe what you want to build and our AI will generate it for you.
|
| 318 |
-
</p>
|
| 319 |
-
<motion.div
|
| 320 |
-
whileHover={{ scale: 1.05 }}
|
| 321 |
-
whileTap={{ scale: 0.95 }}
|
| 322 |
-
>
|
| 323 |
-
<Button
|
| 324 |
-
onClick={handleCreatePrototype}
|
| 325 |
-
className="bg-gradient-to-r from-primary to-indigo-600 text-primary-foreground hover:from-primary/90 hover:to-indigo-700 py-6 px-8 text-lg shadow-xl hover:shadow-2xl"
|
| 326 |
-
>
|
| 327 |
-
<Plus className="h-5 w-5 mr-2" />
|
| 328 |
-
Create Your First Prototype
|
| 329 |
-
</Button>
|
| 330 |
-
</motion.div>
|
| 331 |
-
</motion.div>
|
| 332 |
-
)
|
| 333 |
-
) : (
|
| 334 |
-
<motion.div
|
| 335 |
-
className={viewMode === "grid"
|
| 336 |
-
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
| 337 |
-
: "space-y-4"
|
| 338 |
-
}
|
| 339 |
-
layout
|
| 340 |
-
>
|
| 341 |
-
<AnimatePresence>
|
| 342 |
-
{filteredProjects.map((project) => (
|
| 343 |
-
<motion.div
|
| 344 |
-
key={project.id}
|
| 345 |
-
layout
|
| 346 |
-
initial={{ opacity: 0, scale: 0.8 }}
|
| 347 |
-
animate={{ opacity: 1, scale: 1 }}
|
| 348 |
-
exit={{ opacity: 0, scale: 0.8 }}
|
| 349 |
-
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
| 350 |
-
whileHover={{ y: -5 }}
|
| 351 |
-
whileTap={{ scale: 0.98 }}
|
| 352 |
-
className="w-full"
|
| 353 |
-
>
|
| 354 |
-
<Card
|
| 355 |
-
className={`cursor-pointer hover:shadow-2xl transition-all h-full border-2 ${
|
| 356 |
-
viewMode === "list" ? "flex items-center" : ""
|
| 357 |
-
}`}
|
| 358 |
-
onClick={() => navigate(`/builder?projectId=${project.id}`)}
|
| 359 |
-
>
|
| 360 |
-
{viewMode === "list" ? (
|
| 361 |
-
<>
|
| 362 |
-
<div className="p-6 flex-shrink-0">
|
| 363 |
-
<div className="w-3 h-3 rounded-full bg-primary"></div>
|
| 364 |
-
</div>
|
| 365 |
-
<div className="flex-1 p-6">
|
| 366 |
-
<CardTitle className="flex items-center text-lg">
|
| 367 |
-
{project.name}
|
| 368 |
-
</CardTitle>
|
| 369 |
-
<CardDescription className="flex items-center text-xs mt-2">
|
| 370 |
-
<Calendar className="h-3 w-3 mr-1" />
|
| 371 |
-
{formatDate(project.createdAt)}
|
| 372 |
-
</CardDescription>
|
| 373 |
-
</div>
|
| 374 |
-
<div className="p-6 flex items-center space-x-2">
|
| 375 |
-
<span className="text-sm text-muted-foreground capitalize px-3 py-1 bg-secondary rounded-full">
|
| 376 |
-
{project.status}
|
| 377 |
-
</span>
|
| 378 |
-
<DropdownMenu>
|
| 379 |
-
<DropdownMenuTrigger asChild>
|
| 380 |
-
<Button variant="ghost" size="icon">
|
| 381 |
-
<MoreHorizontal className="h-4 w-4" />
|
| 382 |
-
</Button>
|
| 383 |
-
</DropdownMenuTrigger>
|
| 384 |
-
<DropdownMenuContent align="end">
|
| 385 |
-
<DropdownMenuItem
|
| 386 |
-
className="text-red-600"
|
| 387 |
-
onClick={(e) => {
|
| 388 |
-
e.stopPropagation();
|
| 389 |
-
setProjectToDelete(project.id);
|
| 390 |
-
}}
|
| 391 |
-
>
|
| 392 |
-
<Trash2 className="h-4 w-4 mr-2" />
|
| 393 |
-
Delete
|
| 394 |
-
</DropdownMenuItem>
|
| 395 |
-
</DropdownMenuContent>
|
| 396 |
-
</DropdownMenu>
|
| 397 |
-
</div>
|
| 398 |
-
</>
|
| 399 |
-
) : (
|
| 400 |
-
<>
|
| 401 |
-
<CardHeader>
|
| 402 |
-
<div className="flex justify-between items-start">
|
| 403 |
-
<div>
|
| 404 |
-
<CardTitle className="flex items-center">
|
| 405 |
-
<div className="w-3 h-3 rounded-full bg-primary mr-2"></div>
|
| 406 |
-
{project.name}
|
| 407 |
-
</CardTitle>
|
| 408 |
-
<CardDescription className="flex items-center text-xs pt-1">
|
| 409 |
-
<Calendar className="h-3 w-3 mr-1" />
|
| 410 |
-
{formatDate(project.createdAt)}
|
| 411 |
-
</CardDescription>
|
| 412 |
-
</div>
|
| 413 |
-
<DropdownMenu>
|
| 414 |
-
<DropdownMenuTrigger asChild>
|
| 415 |
-
<Button
|
| 416 |
-
variant="ghost"
|
| 417 |
-
size="icon"
|
| 418 |
-
onClick={(e) => e.stopPropagation()}
|
| 419 |
-
>
|
| 420 |
-
<MoreHorizontal className="h-4 w-4" />
|
| 421 |
-
</Button>
|
| 422 |
-
</DropdownMenuTrigger>
|
| 423 |
-
<DropdownMenuContent align="end">
|
| 424 |
-
<DropdownMenuItem
|
| 425 |
-
className="text-red-600"
|
| 426 |
-
onClick={(e) => {
|
| 427 |
-
e.stopPropagation();
|
| 428 |
-
setProjectToDelete(project.id);
|
| 429 |
-
}}
|
| 430 |
-
>
|
| 431 |
-
<Trash2 className="h-4 w-4 mr-2" />
|
| 432 |
-
Delete
|
| 433 |
-
</DropdownMenuItem>
|
| 434 |
-
</DropdownMenuContent>
|
| 435 |
-
</DropdownMenu>
|
| 436 |
-
</div>
|
| 437 |
-
</CardHeader>
|
| 438 |
-
<CardContent>
|
| 439 |
-
<div className="flex items-center justify-between">
|
| 440 |
-
<span className="text-sm text-muted-foreground capitalize px-3 py-1 bg-secondary rounded-full">
|
| 441 |
-
{project.status}
|
| 442 |
-
</span>
|
| 443 |
-
<Button variant="ghost" size="sm">Open</Button>
|
| 444 |
-
</div>
|
| 445 |
-
</CardContent>
|
| 446 |
-
</>
|
| 447 |
-
)}
|
| 448 |
-
</Card>
|
| 449 |
-
</motion.div>
|
| 450 |
-
))}
|
| 451 |
-
</AnimatePresence>
|
| 452 |
-
</motion.div>
|
| 453 |
-
)}
|
| 454 |
-
</div>
|
| 455 |
-
</main>
|
| 456 |
-
|
| 457 |
-
{/* Name Prototype Dialog */}
|
| 458 |
-
<Dialog open={showNameDialog} onOpenChange={setShowNameDialog}>
|
| 459 |
-
<DialogContent className="sm:max-w-md">
|
| 460 |
-
<DialogHeader>
|
| 461 |
-
<DialogTitle className="text-2xl">Name Your Prototype</DialogTitle>
|
| 462 |
-
<DialogDescription>
|
| 463 |
-
Give your new prototype a descriptive name to help you identify it later.
|
| 464 |
-
</DialogDescription>
|
| 465 |
-
</DialogHeader>
|
| 466 |
-
<div className="py-4">
|
| 467 |
-
<Input
|
| 468 |
-
placeholder="e.g., Landing Page, Dashboard, E-commerce Site"
|
| 469 |
-
value={prototypeName}
|
| 470 |
-
onChange={(e) => setPrototypeName(e.target.value)}
|
| 471 |
-
onKeyPress={(e) => e.key === 'Enter' && handleNameSubmit()}
|
| 472 |
-
className="text-lg py-6"
|
| 473 |
-
/>
|
| 474 |
-
</div>
|
| 475 |
-
<DialogFooter className="gap-3">
|
| 476 |
-
<Button
|
| 477 |
-
variant="outline"
|
| 478 |
-
onClick={() => setShowNameDialog(false)}
|
| 479 |
-
className="py-5 px-6"
|
| 480 |
-
>
|
| 481 |
-
Cancel
|
| 482 |
-
</Button>
|
| 483 |
-
<motion.div
|
| 484 |
-
whileHover={{ scale: 1.02 }}
|
| 485 |
-
whileTap={{ scale: 0.98 }}
|
| 486 |
-
>
|
| 487 |
-
<Button
|
| 488 |
-
onClick={handleNameSubmit}
|
| 489 |
-
disabled={!prototypeName.trim()}
|
| 490 |
-
className="bg-gradient-to-r from-primary to-indigo-600 text-primary-foreground hover:from-primary/90 hover:to-indigo-700 py-5 px-6"
|
| 491 |
-
>
|
| 492 |
-
Create Prototype
|
| 493 |
-
</Button>
|
| 494 |
-
</motion.div>
|
| 495 |
-
</DialogFooter>
|
| 496 |
-
</DialogContent>
|
| 497 |
-
</Dialog>
|
| 498 |
-
|
| 499 |
-
{/* Delete Confirmation Dialog */}
|
| 500 |
-
<Dialog open={!!projectToDelete} onOpenChange={() => setProjectToDelete(null)}>
|
| 501 |
-
<DialogContent>
|
| 502 |
-
<DialogHeader>
|
| 503 |
-
<DialogTitle>Delete Prototype</DialogTitle>
|
| 504 |
-
<DialogDescription>
|
| 505 |
-
Are you sure you want to delete this prototype? This action cannot be undone.
|
| 506 |
-
</DialogDescription>
|
| 507 |
-
</DialogHeader>
|
| 508 |
-
<DialogFooter className="gap-3">
|
| 509 |
-
<Button
|
| 510 |
-
variant="outline"
|
| 511 |
-
onClick={() => setProjectToDelete(null)}
|
| 512 |
-
>
|
| 513 |
-
Cancel
|
| 514 |
-
</Button>
|
| 515 |
-
<Button
|
| 516 |
-
variant="destructive"
|
| 517 |
-
onClick={() => projectToDelete && handleDeleteProject(projectToDelete)}
|
| 518 |
-
>
|
| 519 |
-
Delete
|
| 520 |
-
</Button>
|
| 521 |
-
</DialogFooter>
|
| 522 |
-
</DialogContent>
|
| 523 |
-
</Dialog>
|
| 524 |
-
</div>
|
| 525 |
-
)
|
| 526 |
-
}
|
| 527 |
-
|
| 528 |
-
export default Dashboard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pages/Index.tsx
CHANGED
|
@@ -2,24 +2,16 @@
|
|
| 2 |
|
| 3 |
import { useState, useEffect, useRef } from "react";
|
| 4 |
import { Button } from "@/components/ui/button";
|
| 5 |
-
import { Eye, Code,
|
| 6 |
-
import { MadeWithDyad } from "@/components/made-with-dyad";
|
| 7 |
import { useToast } from "@/hooks/use-toast";
|
| 8 |
import { generateCode, modifyCode, ChatMessage } from "@/services/gemini";
|
| 9 |
-
import {
|
| 10 |
import ChatInterface from "@/components/ChatInterface";
|
| 11 |
import VersionsPanel from "@/components/VersionsPanel";
|
| 12 |
import CodeEditor from "@/components/CodeEditor";
|
| 13 |
import PreviewPanel from "@/components/PreviewPanel";
|
| 14 |
-
import { DiffResult, generateDiff
|
| 15 |
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
| 16 |
-
import { Badge } from "@/components/ui/badge";
|
| 17 |
-
import { useNavigate, useSearchParams } from "react-router-dom";
|
| 18 |
-
import { useAuth } from "@/hooks/useAuth";
|
| 19 |
-
import { useProjects } from "@/hooks/useProjects";
|
| 20 |
import DiffReviewModal from "@/components/DiffReviewModal";
|
| 21 |
-
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
| 22 |
-
import { Input } from "@/components/ui/input";
|
| 23 |
import { ModeToggle } from "@/components/theme-toggle";
|
| 24 |
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
| 25 |
import JSZip from "jszip";
|
|
@@ -41,22 +33,15 @@ const Index = () => {
|
|
| 41 |
|
| 42 |
const [leftTab, setLeftTab] = useState("chat");
|
| 43 |
const [rightTab, setRightTab] = useState("preview");
|
| 44 |
-
const [activeProject, setActiveProject] = useState<Project | null>(null);
|
| 45 |
const { toast } = useToast();
|
| 46 |
const [leftPanelWidth, setLeftPanelWidth] = useState(40);
|
| 47 |
const containerRef = useRef<HTMLDivElement>(null);
|
| 48 |
const isResizingRef = useRef(false);
|
| 49 |
-
|
| 50 |
-
const [
|
| 51 |
-
const { user, loading: authLoading } = useAuth();
|
| 52 |
-
const { projects, versions, fetchVersions, createEmptyProject, createProjectWithFirstVersion, createNewVersion, restoreVersion } = useProjects(user?.id || null);
|
| 53 |
-
const projectIdFromUrl = searchParams.get('projectId');
|
| 54 |
-
|
| 55 |
const [pendingModification, setPendingModification] = useState<{ changes: ChangeProposal[]; newMessages: ChatMessage[] } | null>(null);
|
| 56 |
const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
|
| 57 |
const [diffToReview, setDiffToReview] = useState<DiffResult | null>(null);
|
| 58 |
-
const [showNameDialog, setShowNameDialog] = useState(false);
|
| 59 |
-
const [prototypeName, setPrototypeName] = useState("");
|
| 60 |
const [isAIReady, setIsAIReady] = useState(false);
|
| 61 |
const [selectedElement, setSelectedElement] = useState<{ tagName: string; description: string } | null>(null);
|
| 62 |
|
|
@@ -74,30 +59,7 @@ const Index = () => {
|
|
| 74 |
variant: "destructive",
|
| 75 |
});
|
| 76 |
}
|
| 77 |
-
}, []);
|
| 78 |
-
|
| 79 |
-
useEffect(() => {
|
| 80 |
-
if (!user && !authLoading) {
|
| 81 |
-
navigate('/auth');
|
| 82 |
-
}
|
| 83 |
-
}, [user, authLoading, navigate]);
|
| 84 |
-
|
| 85 |
-
useEffect(() => {
|
| 86 |
-
if (projectIdFromUrl && projects.length > 0) {
|
| 87 |
-
const project = projects.find(p => p.id === projectIdFromUrl);
|
| 88 |
-
if (project && (!activeProject || activeProject.id !== project.id)) {
|
| 89 |
-
loadProject(project);
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
}, [projectIdFromUrl, projects]);
|
| 93 |
-
|
| 94 |
-
useEffect(() => {
|
| 95 |
-
if (activeProject) {
|
| 96 |
-
fetchVersions(activeProject.id);
|
| 97 |
-
} else {
|
| 98 |
-
fetchVersions('');
|
| 99 |
-
}
|
| 100 |
-
}, [activeProject]);
|
| 101 |
|
| 102 |
useEffect(() => {
|
| 103 |
const handleMessage = (event: MessageEvent) => {
|
|
@@ -221,7 +183,7 @@ const Index = () => {
|
|
| 221 |
const newMessages = [...messages, userMessage];
|
| 222 |
setMessages(newMessages);
|
| 223 |
|
| 224 |
-
if (
|
| 225 |
handleModificationRequest(messageContent, newMessages);
|
| 226 |
} else {
|
| 227 |
handleNewProjectRequest(prompt, newMessages);
|
|
@@ -251,16 +213,14 @@ const Index = () => {
|
|
| 251 |
setActivePreviewFile(newFiles[0].name);
|
| 252 |
}
|
| 253 |
|
| 254 |
-
const
|
| 255 |
-
|
| 256 |
-
status: 'completed',
|
| 257 |
-
prompt,
|
| 258 |
code: JSON.stringify(newFiles),
|
| 259 |
-
messages: finalMessages
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
} catch (error: any) {
|
| 265 |
console.error("Error generating code:", error);
|
| 266 |
toast({
|
|
@@ -306,7 +266,7 @@ const Index = () => {
|
|
| 306 |
};
|
| 307 |
|
| 308 |
const handleAcceptChanges = async () => {
|
| 309 |
-
if (!pendingModification
|
| 310 |
|
| 311 |
const { changes, newMessages } = pendingModification;
|
| 312 |
const updatedFiles = [...files];
|
|
@@ -325,9 +285,14 @@ const Index = () => {
|
|
| 325 |
setFiles(updatedFiles);
|
| 326 |
setMessages(finalMessages);
|
| 327 |
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
-
setActiveProject(prev => prev ? { ...prev, code: JSON.stringify(updatedFiles), messages: finalMessages as any } : null);
|
| 331 |
setPendingModification(null);
|
| 332 |
toast({ title: "Changes Accepted", description: "A new version has been saved." });
|
| 333 |
};
|
|
@@ -345,52 +310,35 @@ const Index = () => {
|
|
| 345 |
setIsDiffModalOpen(true);
|
| 346 |
};
|
| 347 |
|
| 348 |
-
const handleRestoreVersion =
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
toast({ title: "Version Restored", description: "The prototype has been restored to a previous version." });
|
|
|
|
|
|
|
| 355 |
}
|
| 356 |
-
} catch (error: any) {
|
| 357 |
-
toast({ title: "Error", description: `Failed to restore version: ${error.message}.`, variant: "destructive" });
|
| 358 |
}
|
| 359 |
};
|
| 360 |
|
| 361 |
-
const
|
| 362 |
-
|
| 363 |
-
setMessages(
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
setActivePreviewFile(parsedFiles[0].name);
|
| 370 |
-
} else { throw new Error("Not an array"); }
|
| 371 |
-
} catch (e) {
|
| 372 |
-
const fallbackFiles = [{ name: 'index.html', code: project.code }];
|
| 373 |
-
setFiles(fallbackFiles);
|
| 374 |
-
setActiveEditorFile('index.html');
|
| 375 |
-
setActivePreviewFile('index.html');
|
| 376 |
-
}
|
| 377 |
setPendingModification(null);
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
const createNewPrototype = () => setShowNameDialog(true);
|
| 382 |
-
|
| 383 |
-
const handleNameSubmit = async () => {
|
| 384 |
-
if (!prototypeName.trim()) return;
|
| 385 |
-
try {
|
| 386 |
-
const newProject = await createEmptyProject(prototypeName);
|
| 387 |
-
setShowNameDialog(false);
|
| 388 |
-
setPrototypeName("");
|
| 389 |
-
navigate(`/builder?projectId=${newProject.id}`);
|
| 390 |
-
loadProject(newProject);
|
| 391 |
-
} catch (err) {
|
| 392 |
-
toast({ title: "Error", description: "Failed to create prototype.", variant: "destructive" });
|
| 393 |
-
}
|
| 394 |
};
|
| 395 |
|
| 396 |
const exportProjectAsZip = () => {
|
|
@@ -403,7 +351,7 @@ const Index = () => {
|
|
| 403 |
zip.file(file.name, file.code);
|
| 404 |
});
|
| 405 |
zip.generateAsync({ type: "blob" }).then(content => {
|
| 406 |
-
saveAs(content,
|
| 407 |
});
|
| 408 |
toast({ title: "Project exported", description: "Your project has been downloaded as a zip file." });
|
| 409 |
};
|
|
@@ -415,7 +363,6 @@ const Index = () => {
|
|
| 415 |
return;
|
| 416 |
}
|
| 417 |
|
| 418 |
-
// Create a Blob directly from the HTML content of the selected file
|
| 419 |
const blob = new Blob([fileToOpen.code], { type: 'text/html' });
|
| 420 |
const url = URL.createObjectURL(blob);
|
| 421 |
window.open(url, '_blank');
|
|
@@ -464,10 +411,6 @@ const Index = () => {
|
|
| 464 |
}
|
| 465 |
};
|
| 466 |
|
| 467 |
-
if (authLoading) {
|
| 468 |
-
return <div className="min-h-screen flex items-center justify-center"><Loader2 className="h-8 w-8 animate-spin" /></div>;
|
| 469 |
-
}
|
| 470 |
-
|
| 471 |
return (
|
| 472 |
<div className="flex flex-col h-screen bg-background text-foreground">
|
| 473 |
<header className="border-b bg-card shadow-sm">
|
|
@@ -490,9 +433,8 @@ const Index = () => {
|
|
| 490 |
</div>
|
| 491 |
)}
|
| 492 |
<ModeToggle />
|
| 493 |
-
<Button size="sm" onClick={
|
| 494 |
-
|
| 495 |
-
<Plus className="h-4 w-4 mr-1" /> New Prototype
|
| 496 |
</Button>
|
| 497 |
</div>
|
| 498 |
</div>
|
|
@@ -508,7 +450,7 @@ const Index = () => {
|
|
| 508 |
</div>
|
| 509 |
<div className="flex-1 overflow-hidden">
|
| 510 |
{leftTab === 'chat' && <ChatInterface prompt={prompt} setPrompt={setPrompt} messages={messages} isLoading={isLoading} onSubmit={handleSubmit} pendingModification={pendingModification} onAccept={handleAcceptChanges} onReject={handleRejectChanges} onReview={handleReviewChanges} selectedElement={selectedElement} onClearSelectedElement={() => setSelectedElement(null)} />}
|
| 511 |
-
{leftTab === 'versions' &&
|
| 512 |
</div>
|
| 513 |
</div>
|
| 514 |
|
|
@@ -531,18 +473,9 @@ const Index = () => {
|
|
| 531 |
{rightTab === 'preview' && <PreviewPanel code={getPreviewCode()} ref={iframeRef} className={isSelectingElement ? "cursor-crosshair" : ""} />}
|
| 532 |
{rightTab === 'code' && <CodeEditor files={files} activeFile={activeEditorFile} onFileChange={handleFileChange} onFileSelect={setActiveEditorFile} onExport={exportProjectAsZip} />}
|
| 533 |
</div>
|
| 534 |
-
{activeProject && <div className="border-t bg-card px-4 py-2 flex items-center justify-between"><div className="flex items-center"><div className="w-2 h-2 rounded-full bg-primary mr-2"></div><span className="text-sm font-medium truncate max-w-xs">{activeProject.name}</span></div></div>}
|
| 535 |
</div>
|
| 536 |
</div>
|
| 537 |
<DiffReviewModal isOpen={isDiffModalOpen} onClose={() => setIsDiffModalOpen(false)} diffResult={diffToReview} onApply={handleAcceptChanges} />
|
| 538 |
-
|
| 539 |
-
<Dialog open={showNameDialog} onOpenChange={setShowNameDialog}>
|
| 540 |
-
<DialogContent>
|
| 541 |
-
<DialogHeader><DialogTitle>Name Your New Prototype</DialogTitle><DialogDescription>Give your new prototype a name to get started.</DialogDescription></DialogHeader>
|
| 542 |
-
<div className="py-4"><Input placeholder="e.g., My Awesome Landing Page" value={prototypeName} onChange={(e) => setPrototypeName(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleNameSubmit()} /></div>
|
| 543 |
-
<DialogFooter><Button variant="outline" onClick={() => setShowNameDialog(false)}>Cancel</Button><Button onClick={handleNameSubmit} disabled={!prototypeName.trim()} className="bg-primary text-primary-foreground hover:bg-primary/90">Create Prototype</Button></DialogFooter>
|
| 544 |
-
</DialogContent>
|
| 545 |
-
</Dialog>
|
| 546 |
</div>
|
| 547 |
);
|
| 548 |
};
|
|
|
|
| 2 |
|
| 3 |
import { useState, useEffect, useRef } from "react";
|
| 4 |
import { Button } from "@/components/ui/button";
|
| 5 |
+
import { Eye, Code, History, Plus, GitBranch, Loader2, WifiOff, MousePointer, ExternalLink, RefreshCw } from "lucide-react";
|
|
|
|
| 6 |
import { useToast } from "@/hooks/use-toast";
|
| 7 |
import { generateCode, modifyCode, ChatMessage } from "@/services/gemini";
|
| 8 |
+
import { ProjectFile, ProjectVersion } from "@/types/project";
|
| 9 |
import ChatInterface from "@/components/ChatInterface";
|
| 10 |
import VersionsPanel from "@/components/VersionsPanel";
|
| 11 |
import CodeEditor from "@/components/CodeEditor";
|
| 12 |
import PreviewPanel from "@/components/PreviewPanel";
|
| 13 |
+
import { DiffResult, generateDiff } from "@/services/diffPatch";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import DiffReviewModal from "@/components/DiffReviewModal";
|
|
|
|
|
|
|
| 15 |
import { ModeToggle } from "@/components/theme-toggle";
|
| 16 |
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
| 17 |
import JSZip from "jszip";
|
|
|
|
| 33 |
|
| 34 |
const [leftTab, setLeftTab] = useState("chat");
|
| 35 |
const [rightTab, setRightTab] = useState("preview");
|
|
|
|
| 36 |
const { toast } = useToast();
|
| 37 |
const [leftPanelWidth, setLeftPanelWidth] = useState(40);
|
| 38 |
const containerRef = useRef<HTMLDivElement>(null);
|
| 39 |
const isResizingRef = useRef(false);
|
| 40 |
+
|
| 41 |
+
const [versions, setVersions] = useState<ProjectVersion[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
const [pendingModification, setPendingModification] = useState<{ changes: ChangeProposal[]; newMessages: ChatMessage[] } | null>(null);
|
| 43 |
const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
|
| 44 |
const [diffToReview, setDiffToReview] = useState<DiffResult | null>(null);
|
|
|
|
|
|
|
| 45 |
const [isAIReady, setIsAIReady] = useState(false);
|
| 46 |
const [selectedElement, setSelectedElement] = useState<{ tagName: string; description: string } | null>(null);
|
| 47 |
|
|
|
|
| 59 |
variant: "destructive",
|
| 60 |
});
|
| 61 |
}
|
| 62 |
+
}, [toast]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
useEffect(() => {
|
| 65 |
const handleMessage = (event: MessageEvent) => {
|
|
|
|
| 183 |
const newMessages = [...messages, userMessage];
|
| 184 |
setMessages(newMessages);
|
| 185 |
|
| 186 |
+
if (files.length > 0) {
|
| 187 |
handleModificationRequest(messageContent, newMessages);
|
| 188 |
} else {
|
| 189 |
handleNewProjectRequest(prompt, newMessages);
|
|
|
|
| 213 |
setActivePreviewFile(newFiles[0].name);
|
| 214 |
}
|
| 215 |
|
| 216 |
+
const newVersion: ProjectVersion = {
|
| 217 |
+
id: new Date().toISOString(),
|
|
|
|
|
|
|
| 218 |
code: JSON.stringify(newFiles),
|
| 219 |
+
messages: finalMessages,
|
| 220 |
+
createdAt: new Date().toISOString(),
|
| 221 |
+
};
|
| 222 |
+
setVersions([newVersion]);
|
| 223 |
+
|
| 224 |
} catch (error: any) {
|
| 225 |
console.error("Error generating code:", error);
|
| 226 |
toast({
|
|
|
|
| 266 |
};
|
| 267 |
|
| 268 |
const handleAcceptChanges = async () => {
|
| 269 |
+
if (!pendingModification) return;
|
| 270 |
|
| 271 |
const { changes, newMessages } = pendingModification;
|
| 272 |
const updatedFiles = [...files];
|
|
|
|
| 285 |
setFiles(updatedFiles);
|
| 286 |
setMessages(finalMessages);
|
| 287 |
|
| 288 |
+
const newVersion: ProjectVersion = {
|
| 289 |
+
id: new Date().toISOString(),
|
| 290 |
+
code: JSON.stringify(updatedFiles),
|
| 291 |
+
messages: finalMessages,
|
| 292 |
+
createdAt: new Date().toISOString(),
|
| 293 |
+
};
|
| 294 |
+
setVersions(prev => [newVersion, ...prev]);
|
| 295 |
|
|
|
|
| 296 |
setPendingModification(null);
|
| 297 |
toast({ title: "Changes Accepted", description: "A new version has been saved." });
|
| 298 |
};
|
|
|
|
| 310 |
setIsDiffModalOpen(true);
|
| 311 |
};
|
| 312 |
|
| 313 |
+
const handleRestoreVersion = (versionId: string) => {
|
| 314 |
+
const versionToRestore = versions.find(v => v.id === versionId);
|
| 315 |
+
if (versionToRestore) {
|
| 316 |
+
try {
|
| 317 |
+
const parsedFiles = JSON.parse(versionToRestore.code);
|
| 318 |
+
setFiles(parsedFiles);
|
| 319 |
+
setMessages(versionToRestore.messages);
|
| 320 |
+
if (parsedFiles.length > 0) {
|
| 321 |
+
setActiveEditorFile(parsedFiles[0].name);
|
| 322 |
+
setActivePreviewFile(parsedFiles[0].name);
|
| 323 |
+
}
|
| 324 |
toast({ title: "Version Restored", description: "The prototype has been restored to a previous version." });
|
| 325 |
+
} catch (error) {
|
| 326 |
+
toast({ title: "Error", description: "Failed to parse version data.", variant: "destructive" });
|
| 327 |
}
|
|
|
|
|
|
|
| 328 |
}
|
| 329 |
};
|
| 330 |
|
| 331 |
+
const handleStartOver = () => {
|
| 332 |
+
setPrompt("");
|
| 333 |
+
setMessages([]);
|
| 334 |
+
setIsLoading(false);
|
| 335 |
+
setFiles([]);
|
| 336 |
+
setActiveEditorFile("");
|
| 337 |
+
setActivePreviewFile("");
|
| 338 |
+
setVersions([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
setPendingModification(null);
|
| 340 |
+
setSelectedElement(null);
|
| 341 |
+
toast({ title: "Started Over", description: "The builder has been reset." });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
};
|
| 343 |
|
| 344 |
const exportProjectAsZip = () => {
|
|
|
|
| 351 |
zip.file(file.name, file.code);
|
| 352 |
});
|
| 353 |
zip.generateAsync({ type: "blob" }).then(content => {
|
| 354 |
+
saveAs(content, `project.zip`);
|
| 355 |
});
|
| 356 |
toast({ title: "Project exported", description: "Your project has been downloaded as a zip file." });
|
| 357 |
};
|
|
|
|
| 363 |
return;
|
| 364 |
}
|
| 365 |
|
|
|
|
| 366 |
const blob = new Blob([fileToOpen.code], { type: 'text/html' });
|
| 367 |
const url = URL.createObjectURL(blob);
|
| 368 |
window.open(url, '_blank');
|
|
|
|
| 411 |
}
|
| 412 |
};
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
return (
|
| 415 |
<div className="flex flex-col h-screen bg-background text-foreground">
|
| 416 |
<header className="border-b bg-card shadow-sm">
|
|
|
|
| 433 |
</div>
|
| 434 |
)}
|
| 435 |
<ModeToggle />
|
| 436 |
+
<Button size="sm" onClick={handleStartOver} variant="outline">
|
| 437 |
+
<RefreshCw className="h-4 w-4 mr-1" /> Start Over
|
|
|
|
| 438 |
</Button>
|
| 439 |
</div>
|
| 440 |
</div>
|
|
|
|
| 450 |
</div>
|
| 451 |
<div className="flex-1 overflow-hidden">
|
| 452 |
{leftTab === 'chat' && <ChatInterface prompt={prompt} setPrompt={setPrompt} messages={messages} isLoading={isLoading} onSubmit={handleSubmit} pendingModification={pendingModification} onAccept={handleAcceptChanges} onReject={handleRejectChanges} onReview={handleReviewChanges} selectedElement={selectedElement} onClearSelectedElement={() => setSelectedElement(null)} />}
|
| 453 |
+
{leftTab === 'versions' && <VersionsPanel versions={versions} onRestoreVersion={handleRestoreVersion} />}
|
| 454 |
</div>
|
| 455 |
</div>
|
| 456 |
|
|
|
|
| 473 |
{rightTab === 'preview' && <PreviewPanel code={getPreviewCode()} ref={iframeRef} className={isSelectingElement ? "cursor-crosshair" : ""} />}
|
| 474 |
{rightTab === 'code' && <CodeEditor files={files} activeFile={activeEditorFile} onFileChange={handleFileChange} onFileSelect={setActiveEditorFile} onExport={exportProjectAsZip} />}
|
| 475 |
</div>
|
|
|
|
| 476 |
</div>
|
| 477 |
</div>
|
| 478 |
<DiffReviewModal isOpen={isDiffModalOpen} onClose={() => setIsDiffModalOpen(false)} diffResult={diffToReview} onApply={handleAcceptChanges} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
</div>
|
| 480 |
);
|
| 481 |
};
|
src/pages/Landing.tsx
DELETED
|
@@ -1,288 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { Button } from "@/components/ui/button"
|
| 4 |
-
import { Badge } from "@/components/ui/badge"
|
| 5 |
-
import { Code, Zap, Users, ArrowRight } from "lucide-react"
|
| 6 |
-
import { useNavigate } from "react-router-dom"
|
| 7 |
-
import { useAuth } from "@/hooks/useAuth"
|
| 8 |
-
import { motion } from "framer-motion"
|
| 9 |
-
import AnimatedBackground from "@/components/AnimatedBackground"
|
| 10 |
-
import { useEffect } from "react"
|
| 11 |
-
|
| 12 |
-
const Landing = () => {
|
| 13 |
-
const navigate = useNavigate()
|
| 14 |
-
const { user, loading } = useAuth()
|
| 15 |
-
|
| 16 |
-
// Redirect logged-in users to dashboard
|
| 17 |
-
useEffect(() => {
|
| 18 |
-
if (user && !loading) {
|
| 19 |
-
navigate('/dashboard')
|
| 20 |
-
}
|
| 21 |
-
}, [user, loading, navigate])
|
| 22 |
-
|
| 23 |
-
const handleGetStarted = () => {
|
| 24 |
-
if (user) {
|
| 25 |
-
navigate('/dashboard')
|
| 26 |
-
} else {
|
| 27 |
-
navigate('/auth')
|
| 28 |
-
}
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
// Show loading state while checking auth
|
| 32 |
-
if (loading) {
|
| 33 |
-
return (
|
| 34 |
-
<div className="min-h-screen flex items-center justify-center bg-background">
|
| 35 |
-
<div className="text-center">
|
| 36 |
-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
| 37 |
-
<p className="text-muted-foreground">Loading...</p>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
)
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
const fadeInUp = {
|
| 44 |
-
initial: { opacity: 0, y: 30 },
|
| 45 |
-
animate: { opacity: 1, y: 0 },
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
return (
|
| 49 |
-
<div className="min-h-screen text-gray-100 overflow-x-hidden">
|
| 50 |
-
<AnimatedBackground />
|
| 51 |
-
|
| 52 |
-
{/* Navigation */}
|
| 53 |
-
<motion.nav
|
| 54 |
-
initial={{ y: -100, opacity: 0 }}
|
| 55 |
-
animate={{ y: 0, opacity: 1 }}
|
| 56 |
-
transition={{ duration: 0.8, ease: "easeOut" }}
|
| 57 |
-
className="sticky top-0 z-50 border-b border-gray-800 bg-gray-900/80 backdrop-blur-xl"
|
| 58 |
-
>
|
| 59 |
-
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
|
| 60 |
-
<motion.div
|
| 61 |
-
className="flex items-center space-x-2"
|
| 62 |
-
whileHover={{ scale: 1.05 }}
|
| 63 |
-
whileTap={{ scale: 0.95 }}
|
| 64 |
-
>
|
| 65 |
-
<div className="w-10 h-10 flex items-center justify-center">
|
| 66 |
-
<img src="/front-pilot.svg" alt="FrontPilot Logo" className="w-10 h-10" />
|
| 67 |
-
</div>
|
| 68 |
-
<div className="flex items-center">
|
| 69 |
-
<span className="font-bold text-2xl bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400">
|
| 70 |
-
FrontPilot
|
| 71 |
-
</span>
|
| 72 |
-
<span className="ml-2 text-[0.6rem] bg-yellow-500/20 text-yellow-500 font-bold px-1.5 py-0.5 rounded-full backdrop-blur-sm border border-yellow-500/30">BETA</span>
|
| 73 |
-
</div>
|
| 74 |
-
</motion.div>
|
| 75 |
-
<div className="flex items-center space-x-3">
|
| 76 |
-
<Button
|
| 77 |
-
variant="ghost"
|
| 78 |
-
onClick={() => navigate('/auth')}
|
| 79 |
-
className="text-gray-300 hover:text-white hover:bg-gray-800 transition-all"
|
| 80 |
-
>
|
| 81 |
-
Sign In
|
| 82 |
-
</Button>
|
| 83 |
-
<motion.div
|
| 84 |
-
whileHover={{ scale: 1.05 }}
|
| 85 |
-
whileTap={{ scale: 0.95 }}
|
| 86 |
-
>
|
| 87 |
-
<Button
|
| 88 |
-
onClick={handleGetStarted}
|
| 89 |
-
className="bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-600 text-lg py-6 px-8 shadow-xl hover:shadow-2xl"
|
| 90 |
-
>
|
| 91 |
-
Start Building Free
|
| 92 |
-
</Button>
|
| 93 |
-
</motion.div>
|
| 94 |
-
</div>
|
| 95 |
-
</div>
|
| 96 |
-
</motion.nav>
|
| 97 |
-
|
| 98 |
-
{/* Hero Section */}
|
| 99 |
-
<section className="relative py-20 md:py-32 overflow-hidden">
|
| 100 |
-
<div className="container mx-auto px-4 text-center max-w-5xl relative z-10">
|
| 101 |
-
<motion.div
|
| 102 |
-
variants={fadeInUp}
|
| 103 |
-
initial="initial"
|
| 104 |
-
animate="animate"
|
| 105 |
-
transition={{ duration: 0.8, ease: "easeOut" }}
|
| 106 |
-
>
|
| 107 |
-
<Badge className="mb-6 bg-gray-800 text-blue-400 border-gray-700 px-4 py-1 text-sm">
|
| 108 |
-
<Zap className="h-3.5 w-3.5 mr-1.5" />
|
| 109 |
-
AI-Powered Development
|
| 110 |
-
</Badge>
|
| 111 |
-
</motion.div>
|
| 112 |
-
|
| 113 |
-
<motion.h1
|
| 114 |
-
variants={fadeInUp}
|
| 115 |
-
initial="initial"
|
| 116 |
-
animate="animate"
|
| 117 |
-
transition={{ duration: 0.8, ease: "easeOut", delay: 0.1 }}
|
| 118 |
-
className="text-5xl md:text-7xl font-extrabold text-white mb-6 leading-tight"
|
| 119 |
-
>
|
| 120 |
-
Build Websites with <span className="bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">Front Pilot</span>
|
| 121 |
-
</motion.h1>
|
| 122 |
-
|
| 123 |
-
<motion.p
|
| 124 |
-
variants={fadeInUp}
|
| 125 |
-
initial="initial"
|
| 126 |
-
animate="animate"
|
| 127 |
-
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
|
| 128 |
-
className="text-xl md:text-2xl text-gray-300 mb-10 max-w-3xl mx-auto"
|
| 129 |
-
>
|
| 130 |
-
Create stunning websites in seconds with our AI-powered prototyping tool.
|
| 131 |
-
No coding required - just describe what you want and watch it come to life.
|
| 132 |
-
</motion.p>
|
| 133 |
-
|
| 134 |
-
<motion.div
|
| 135 |
-
variants={fadeInUp}
|
| 136 |
-
initial="initial"
|
| 137 |
-
animate="animate"
|
| 138 |
-
transition={{ duration: 0.8, ease: "easeOut", delay: 0.3 }}
|
| 139 |
-
className="flex justify-center"
|
| 140 |
-
>
|
| 141 |
-
<motion.div
|
| 142 |
-
whileHover={{ scale: 1.05 }}
|
| 143 |
-
whileTap={{ scale: 0.95 }}
|
| 144 |
-
>
|
| 145 |
-
<Button
|
| 146 |
-
size="lg"
|
| 147 |
-
onClick={handleGetStarted}
|
| 148 |
-
className="bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-600 text-lg py-6 px-8 shadow-xl hover:shadow-2xl"
|
| 149 |
-
>
|
| 150 |
-
Start Building Free
|
| 151 |
-
</Button>
|
| 152 |
-
</motion.div>
|
| 153 |
-
</motion.div>
|
| 154 |
-
</div>
|
| 155 |
-
</section>
|
| 156 |
-
|
| 157 |
-
{/* Features Section */}
|
| 158 |
-
<section className="py-28 relative">
|
| 159 |
-
<div className="container mx-auto px-4">
|
| 160 |
-
<motion.div
|
| 161 |
-
initial={{ opacity: 0 }}
|
| 162 |
-
whileInView={{ opacity: 1 }}
|
| 163 |
-
viewport={{ once: true, amount: 0.3 }}
|
| 164 |
-
transition={{ duration: 0.7 }}
|
| 165 |
-
className="text-center mb-20"
|
| 166 |
-
>
|
| 167 |
-
<h2 className="text-4xl md:text-5xl font-bold text-white mb-6">
|
| 168 |
-
Powerful Features
|
| 169 |
-
</h2>
|
| 170 |
-
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
|
| 171 |
-
Everything you need to create amazing websites without writing code
|
| 172 |
-
</p>
|
| 173 |
-
</motion.div>
|
| 174 |
-
|
| 175 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 176 |
-
{[
|
| 177 |
-
{
|
| 178 |
-
icon: Code,
|
| 179 |
-
title: "AI-Powered Creation",
|
| 180 |
-
description: "Describe your website in plain English and our AI will generate a fully functional prototype in seconds.",
|
| 181 |
-
delay: 0.1,
|
| 182 |
-
upcoming: false
|
| 183 |
-
},
|
| 184 |
-
{
|
| 185 |
-
icon: Zap,
|
| 186 |
-
title: "Instant Prototyping",
|
| 187 |
-
description: "Get a working prototype immediately. No more waiting for development cycles or design iterations.",
|
| 188 |
-
delay: 0.2,
|
| 189 |
-
upcoming: false
|
| 190 |
-
},
|
| 191 |
-
{
|
| 192 |
-
icon: Users,
|
| 193 |
-
title: "Team Collaboration",
|
| 194 |
-
description: "Share projects with your team, get feedback, and iterate together in real-time.",
|
| 195 |
-
delay: 0.3,
|
| 196 |
-
upcoming: true
|
| 197 |
-
}
|
| 198 |
-
].map((feature, index) => (
|
| 199 |
-
<motion.div
|
| 200 |
-
key={index}
|
| 201 |
-
initial={{ opacity: 0, y: 50 }}
|
| 202 |
-
whileInView={{ opacity: 1, y: 0 }}
|
| 203 |
-
viewport={{ once: true, amount: 0.3 }}
|
| 204 |
-
transition={{ duration: 0.5, delay: feature.delay }}
|
| 205 |
-
whileHover={{ y: -10 }}
|
| 206 |
-
className="relative bg-gray-800/50 backdrop-blur-sm border border-gray-700 rounded-2xl p-6 overflow-hidden"
|
| 207 |
-
>
|
| 208 |
-
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 transition-opacity duration-300 hover:opacity-100" />
|
| 209 |
-
<div className="relative z-10">
|
| 210 |
-
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-blue-500/10">
|
| 211 |
-
<feature.icon className="h-6 w-6 text-blue-400" />
|
| 212 |
-
</div>
|
| 213 |
-
<div className="flex items-center mb-2">
|
| 214 |
-
<h3 className="text-xl font-bold text-white">{feature.title}</h3>
|
| 215 |
-
{feature.upcoming && (
|
| 216 |
-
<Badge className="ml-2 text-xs bg-orange-500/20 text-orange-300 border border-orange-500/30">
|
| 217 |
-
UPCOMING
|
| 218 |
-
</Badge>
|
| 219 |
-
)}
|
| 220 |
-
</div>
|
| 221 |
-
<p className="text-gray-300">{feature.description}</p>
|
| 222 |
-
</div>
|
| 223 |
-
</motion.div>
|
| 224 |
-
))}
|
| 225 |
-
</div>
|
| 226 |
-
</div>
|
| 227 |
-
</section>
|
| 228 |
-
|
| 229 |
-
{/* CTA Section */}
|
| 230 |
-
<section className="py-28 relative overflow-hidden">
|
| 231 |
-
<div className="container mx-auto px-4 text-center relative z-10">
|
| 232 |
-
<motion.h2
|
| 233 |
-
initial={{ opacity: 0, y: 30 }}
|
| 234 |
-
whileInView={{ opacity: 1, y: 0 }}
|
| 235 |
-
viewport={{ once: true, amount: 0.3 }}
|
| 236 |
-
transition={{ duration: 0.7 }}
|
| 237 |
-
className="text-4xl md:text-5xl font-bold text-white mb-6"
|
| 238 |
-
>
|
| 239 |
-
Ready to build your next website?
|
| 240 |
-
</motion.h2>
|
| 241 |
-
<motion.p
|
| 242 |
-
initial={{ opacity: 0, y: 30 }}
|
| 243 |
-
whileInView={{ opacity: 1, y: 0 }}
|
| 244 |
-
viewport={{ once: true, amount: 0.3 }}
|
| 245 |
-
transition={{ duration: 0.7, delay: 0.1 }}
|
| 246 |
-
className="text-xl text-gray-300 mb-10 max-w-2xl mx-auto"
|
| 247 |
-
>
|
| 248 |
-
Join thousands of creators who are building amazing websites with FrontPilot
|
| 249 |
-
</motion.p>
|
| 250 |
-
<motion.div
|
| 251 |
-
initial={{ opacity: 0, y: 30 }}
|
| 252 |
-
whileInView={{ opacity: 1, y: 0 }}
|
| 253 |
-
viewport={{ once: true, amount: 0.3 }}
|
| 254 |
-
transition={{ duration: 0.7, delay: 0.2 }}
|
| 255 |
-
whileHover={{ scale: 1.05 }}
|
| 256 |
-
whileTap={{ scale: 0.95 }}
|
| 257 |
-
>
|
| 258 |
-
<Button
|
| 259 |
-
size="lg"
|
| 260 |
-
onClick={handleGetStarted}
|
| 261 |
-
className="bg-white text-gray-900 hover:bg-gray-100 text-lg py-6 px-10 shadow-2xl hover:shadow-3xl"
|
| 262 |
-
>
|
| 263 |
-
Get Started Free
|
| 264 |
-
</Button>
|
| 265 |
-
</motion.div>
|
| 266 |
-
</div>
|
| 267 |
-
</section>
|
| 268 |
-
|
| 269 |
-
{/* Footer */}
|
| 270 |
-
<footer className="py-12 bg-gray-900/50 border-t border-gray-800">
|
| 271 |
-
<div className="container mx-auto px-4 text-center">
|
| 272 |
-
<div className="flex items-center justify-center space-x-3 mb-6">
|
| 273 |
-
<div className="w-10 h-10 flex items-center justify-center">
|
| 274 |
-
<img src="/front-pilot.svg" alt="FrontPilot Logo" className="w-10 h-10" />
|
| 275 |
-
</div>
|
| 276 |
-
<span className="font-bold text-2xl text-white">FrontPilot</span>
|
| 277 |
-
</div>
|
| 278 |
-
<p className="mb-6 max-w-lg mx-auto text-gray-400">AI-powered rapid prototyping for modern web development</p>
|
| 279 |
-
<div className="text-sm text-gray-500">
|
| 280 |
-
© {new Date().getFullYear()} Halopai Inc. All rights reserved.
|
| 281 |
-
</div>
|
| 282 |
-
</div>
|
| 283 |
-
</footer>
|
| 284 |
-
</div>
|
| 285 |
-
)
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
export default Landing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pages/VerifyEmail.tsx
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect } from "react"
|
| 4 |
-
import { Button } from "@/components/ui/button"
|
| 5 |
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
| 6 |
-
import { useNavigate } from "react-router-dom"
|
| 7 |
-
import { useAuth } from "@/hooks/useAuth"
|
| 8 |
-
import { motion } from "framer-motion"
|
| 9 |
-
import AnimatedBackground from "@/components/AnimatedBackground"
|
| 10 |
-
import { Badge } from "@/components/ui/badge"
|
| 11 |
-
import { Mail, CheckCircle, RefreshCw } from "lucide-react"
|
| 12 |
-
|
| 13 |
-
const VerifyEmail = () => {
|
| 14 |
-
const [isVerifying, setIsVerifying] = useState(false)
|
| 15 |
-
const [verificationStatus, setVerificationStatus] = useState<"pending" | "verified" | "error">("pending")
|
| 16 |
-
const navigate = useNavigate()
|
| 17 |
-
const { user, signOut } = useAuth()
|
| 18 |
-
|
| 19 |
-
useEffect(() => {
|
| 20 |
-
// If user is already verified, redirect to dashboard
|
| 21 |
-
if (user && user.email_confirmed_at) {
|
| 22 |
-
navigate('/dashboard')
|
| 23 |
-
}
|
| 24 |
-
}, [user, navigate])
|
| 25 |
-
|
| 26 |
-
const handleResendEmail = async () => {
|
| 27 |
-
setIsVerifying(true)
|
| 28 |
-
try {
|
| 29 |
-
// In a real app, you would call a function to resend verification email
|
| 30 |
-
// For now, we'll just simulate it
|
| 31 |
-
await new Promise(resolve => setTimeout(resolve, 1500))
|
| 32 |
-
setVerificationStatus("pending")
|
| 33 |
-
} catch (error) {
|
| 34 |
-
setVerificationStatus("error")
|
| 35 |
-
} finally {
|
| 36 |
-
setIsVerifying(false)
|
| 37 |
-
}
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
const handleSignOut = async () => {
|
| 41 |
-
await signOut()
|
| 42 |
-
navigate('/auth')
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
return (
|
| 46 |
-
<div className="min-h-screen w-full flex items-center justify-center p-4 relative overflow-hidden">
|
| 47 |
-
<AnimatedBackground />
|
| 48 |
-
|
| 49 |
-
<motion.div
|
| 50 |
-
initial={{ opacity: 0, y: 20 }}
|
| 51 |
-
animate={{ opacity: 1, y: 0 }}
|
| 52 |
-
transition={{ duration: 0.5 }}
|
| 53 |
-
className="w-full max-w-md z-10"
|
| 54 |
-
>
|
| 55 |
-
<Card className="shadow-2xl backdrop-blur-xl border border-gray-800 bg-gray-900/80">
|
| 56 |
-
<CardHeader className="text-center pb-6">
|
| 57 |
-
<motion.div
|
| 58 |
-
className="flex justify-center mb-4"
|
| 59 |
-
whileHover={{ scale: 1.1 }}
|
| 60 |
-
whileTap={{ scale: 0.9 }}
|
| 61 |
-
>
|
| 62 |
-
<div className="w-16 h-16 flex items-center justify-center">
|
| 63 |
-
<img src="/front-pilot.svg" alt="FrontPilot Logo" className="w-16 h-16" />
|
| 64 |
-
</div>
|
| 65 |
-
</motion.div>
|
| 66 |
-
<div className="flex items-center justify-center">
|
| 67 |
-
<CardTitle className="text-3xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400">
|
| 68 |
-
Verify Your Email
|
| 69 |
-
</CardTitle>
|
| 70 |
-
<span className="ml-3 text-[0.6rem] bg-yellow-500/20 text-yellow-500 font-bold px-1.5 py-0.5 rounded-full backdrop-blur-sm border border-yellow-500/30">BETA</span>
|
| 71 |
-
</div>
|
| 72 |
-
<CardDescription className="text-base text-gray-400">
|
| 73 |
-
Please check your email for a verification link
|
| 74 |
-
</CardDescription>
|
| 75 |
-
</CardHeader>
|
| 76 |
-
|
| 77 |
-
<CardContent className="space-y-6">
|
| 78 |
-
<div className="text-center">
|
| 79 |
-
<div className="mx-auto w-20 h-20 bg-blue-500/10 rounded-full flex items-center justify-center mb-4">
|
| 80 |
-
<Mail className="h-10 w-10 text-blue-400" />
|
| 81 |
-
</div>
|
| 82 |
-
<p className="text-gray-300 mb-4">
|
| 83 |
-
We've sent a verification email to <span className="font-medium text-white">{user?.email}</span>
|
| 84 |
-
</p>
|
| 85 |
-
<p className="text-sm text-gray-400">
|
| 86 |
-
Please click the link in the email to verify your account and activate your FrontPilot access.
|
| 87 |
-
</p>
|
| 88 |
-
</div>
|
| 89 |
-
|
| 90 |
-
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-4">
|
| 91 |
-
<h3 className="font-medium text-gray-200 mb-2 flex items-center">
|
| 92 |
-
<CheckCircle className="h-4 w-4 mr-2 text-green-400" />
|
| 93 |
-
Didn't receive the email?
|
| 94 |
-
</h3>
|
| 95 |
-
<ul className="text-sm text-gray-400 space-y-1">
|
| 96 |
-
<li>• Check your spam or junk folder</li>
|
| 97 |
-
<li>• Wait a few minutes for delivery</li>
|
| 98 |
-
<li>• Ensure you entered the correct email address</li>
|
| 99 |
-
</ul>
|
| 100 |
-
</div>
|
| 101 |
-
</CardContent>
|
| 102 |
-
|
| 103 |
-
<CardFooter className="flex flex-col pt-4">
|
| 104 |
-
<motion.div
|
| 105 |
-
whileHover={{ scale: 1.02 }}
|
| 106 |
-
whileTap={{ scale: 0.98 }}
|
| 107 |
-
className="w-full"
|
| 108 |
-
>
|
| 109 |
-
<Button
|
| 110 |
-
onClick={() => window.location.reload()}
|
| 111 |
-
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-600 py-6 text-lg shadow-lg hover:shadow-xl"
|
| 112 |
-
>
|
| 113 |
-
I've Verified My Email - Continue
|
| 114 |
-
</Button>
|
| 115 |
-
</motion.div>
|
| 116 |
-
|
| 117 |
-
<motion.div
|
| 118 |
-
whileHover={{ scale: 1.02 }}
|
| 119 |
-
whileTap={{ scale: 0.98 }}
|
| 120 |
-
className="mt-4 w-full"
|
| 121 |
-
>
|
| 122 |
-
<Button
|
| 123 |
-
variant="outline"
|
| 124 |
-
onClick={handleResendEmail}
|
| 125 |
-
disabled={isVerifying}
|
| 126 |
-
className="w-full border-gray-600 text-gray-300 hover:bg-gray-800 hover:text-white hover:border-gray-500 py-5"
|
| 127 |
-
>
|
| 128 |
-
{isVerifying ? (
|
| 129 |
-
<>
|
| 130 |
-
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
| 131 |
-
Sending...
|
| 132 |
-
</>
|
| 133 |
-
) : (
|
| 134 |
-
"Resend Verification Email"
|
| 135 |
-
)}
|
| 136 |
-
</Button>
|
| 137 |
-
</motion.div>
|
| 138 |
-
|
| 139 |
-
<motion.div
|
| 140 |
-
whileHover={{ scale: 1.05 }}
|
| 141 |
-
whileTap={{ scale: 0.9 }}
|
| 142 |
-
className="mt-4 w-full"
|
| 143 |
-
>
|
| 144 |
-
<Button
|
| 145 |
-
variant="ghost"
|
| 146 |
-
onClick={handleSignOut}
|
| 147 |
-
className="w-full text-gray-400 hover:text-white hover:bg-gray-800/50 py-5"
|
| 148 |
-
>
|
| 149 |
-
Sign Out
|
| 150 |
-
</Button>
|
| 151 |
-
</motion.div>
|
| 152 |
-
</CardFooter>
|
| 153 |
-
</Card>
|
| 154 |
-
</motion.div>
|
| 155 |
-
</div>
|
| 156 |
-
)
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
export default VerifyEmail
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/types/project.ts
CHANGED
|
@@ -3,28 +3,14 @@ export interface ProjectFile {
|
|
| 3 |
code: string;
|
| 4 |
}
|
| 5 |
|
| 6 |
-
export interface
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
name: string;
|
| 10 |
-
status: 'pending' | 'completed' | 'failed';
|
| 11 |
-
prompt: string;
|
| 12 |
-
code: string;
|
| 13 |
-
createdAt: Date;
|
| 14 |
-
updatedAt: Date;
|
| 15 |
-
messages: ChatMessage[];
|
| 16 |
}
|
| 17 |
|
| 18 |
export interface ProjectVersion {
|
| 19 |
id: string;
|
| 20 |
-
|
| 21 |
-
code: string;
|
| 22 |
messages: ChatMessage[];
|
| 23 |
createdAt: string;
|
| 24 |
-
// Removed 'name' property as it doesn't exist in the database schema
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
export interface ChatMessage {
|
| 28 |
-
role: 'user' | 'assistant';
|
| 29 |
-
content: string;
|
| 30 |
}
|
|
|
|
| 3 |
code: string;
|
| 4 |
}
|
| 5 |
|
| 6 |
+
export interface ChatMessage {
|
| 7 |
+
role: 'user' | 'assistant';
|
| 8 |
+
content: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
export interface ProjectVersion {
|
| 12 |
id: string;
|
| 13 |
+
code: string; // This will be a JSON string of ProjectFile[]
|
|
|
|
| 14 |
messages: ChatMessage[];
|
| 15 |
createdAt: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|