diff --git a/package-lock.json b/package-lock.json index 0028217..653181b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "date-fns": "^2.30.0", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", + "lucide-react": "^0.542.0", "monaco-editor": "^0.43.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -4392,6 +4393,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.542.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", + "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index a00b807..f413355 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "date-fns": "^2.30.0", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", + "lucide-react": "^0.542.0", "monaco-editor": "^0.43.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.jsx b/src/App.jsx index 64f4d48..65b0c10 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,12 +16,11 @@ import SharedProjects from './pages/SharedProjects'; import Settings from './pages/Settings'; import NotFound from './pages/NotFound'; import ResetPassword from './pages/ResetPassword'; +import GettingStarted from './pages/GettingStarted'; // Components import ProtectedRoute from './components/Shared/ProtectedRoute'; import Layout from './components/Shared/Layout'; -import ErrorBoundary from './components/Shared/ErrorBoundary'; -import { LoadingSpinner } from './components/Shared/LoadingStates'; const App = () => { const { loading, currentUser } = useContext(AuthContext); @@ -29,92 +28,58 @@ const App = () => { if (loading) { return ( - - - Initializing CodeConclave - Almost there... + + + Initializing CodeConclave + Almost there... ); } return ( - - - {/* Landing Page - The initial animated page */} - : - } /> - - {/* Home Page - With Login/Register tabs */} - : ( - - - - ) - } /> - - {/* Password Reset */} - : ( - - - - ) - } /> - - {/* Auth Routes (for direct access) */} - : ( - - - - ) - } /> - : ( - - - - ) - } /> - - {/* Protected Routes */} - - - - - - }> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - + + {/* Landing Page */} + : } + /> + + {/* Home Page */} + : } + /> + + {/* Password Reset */} + : } + /> + + {/* Auth Routes */} + : } + /> + : } + /> + + {/* Protected Routes */} + }> + } /> + } /> + } /> + } /> + } /> } /> - - + + ); }; +// Loading styles const LoadingScreen = styled.div` display: flex; flex-direction: column; @@ -126,34 +91,36 @@ const LoadingScreen = styled.div` : 'linear-gradient(to bottom, #f8fafc, #e2e8f0)' }; color: ${props => props.$isDarkMode ? 'white' : '#1a202c'}; - transition: background 0.3s ease, color 0.3s ease; `; -const LoadingText = styled.h2` - font-size: 1.5rem; - margin: 1rem 0 0.5rem 0; - font-weight: 600; - color: ${props => props.$isDarkMode ? 'white' : '#2d3748'}; - transition: color 0.3s ease; - margin-top: 1rem; - margin-bottom: 0.5rem; +const Spinner = styled.div` + width: 50px; + height: 50px; + border: 5px solid rgba(0,0,0,0.1); + border-radius: 50%; + border-top-color: #3182ce; + animation: spin 1s ease-in-out infinite; + margin-bottom: 20px; + + @keyframes spin { + to { transform: rotate(360deg); } + } +`; + +const LoadingText = styled.p` + font-size: 1.2rem; + color: #4a5568; `; const LoadingSubtext = styled.p` font-size: 1rem; - margin: 0; - color: ${props => props.$isDarkMode ? 'rgba(255, 255, 255, 0.8)' : '#718096'}; + color: #718096; animation: pulse 2s ease-in-out infinite; - transition: color 0.3s ease; - + @keyframes pulse { - 0%, 100% { - opacity: ${props => props.$isDarkMode ? '0.8' : '0.9'}; - } - 50% { - opacity: ${props => props.$isDarkMode ? '0.4' : '0.5'}; - } + 0%, 100% { opacity: 0.9; } + 50% { opacity: 0.5; } } `; -export default App; \ No newline at end of file +export default App; diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index a3a4b78..f040fae 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -83,7 +83,7 @@ const Login = props => { try { await login(formData); - navigate('/dashboard'); + navigate('/getting-started'); } catch (err) { console.error('Login error:', err); } finally { diff --git a/src/components/Auth/Register.jsx b/src/components/Auth/Register.jsx index 02c3e40..edec8fe 100644 --- a/src/components/Auth/Register.jsx +++ b/src/components/Auth/Register.jsx @@ -99,7 +99,7 @@ const Register = () => { // Remove confirmPassword before sending to API const { confirmPassword, ...userData } = formData; await register(userData); - navigate('/dashboard'); + navigate('/getting-started'); } catch (err) { console.error('Registration error:', err); // Error is handled by AuthContext diff --git a/src/components/Settings/GoogleDriveIntegration.jsx b/src/components/Settings/GoogleDriveIntegration.jsx new file mode 100644 index 0000000..56ee990 --- /dev/null +++ b/src/components/Settings/GoogleDriveIntegration.jsx @@ -0,0 +1,21 @@ +const GoogleDriveIntegration = ({ onConnected }) => { + return ( +
+ +
+ ); +}; + +export default GoogleDriveIntegration; diff --git a/src/components/Settings/Settings.jsx b/src/components/Settings/Settings.jsx index 69de4fe..577d2dc 100644 --- a/src/components/Settings/Settings.jsx +++ b/src/components/Settings/Settings.jsx @@ -25,6 +25,8 @@ import { FaTimes, } from 'react-icons/fa'; import { useLocation } from 'react-router-dom'; +import GoogleDriveIntegration from "./GoogleDriveIntegration"; + const Settings = () => { const { currentUser } = useContext(AuthContext); @@ -199,9 +201,11 @@ const Settings = () => { }; const handleGoogleDriveCallback = async () => { + const handleGoogleDriveCallback = async () => { const urlParams = new URLSearchParams(window.location.search); const googleDriveStatusParam = urlParams.get('googleDrive'); const tokensParam = urlParams.get('tokens'); + const authCode = urlParams.get('code'); if (googleDriveStatusParam === 'connected' && tokensParam) { try { @@ -236,6 +240,20 @@ const Settings = () => { } return false; // Not a callback }; + if (authCode) { + try { + const tokens = await googleDriveService.exchangeCodeForTokens(authCode); + setGoogleDriveSuccess('Google Drive connected successfully!'); + setGoogleDriveStatus({ isConnected: true, tokenExpiry: tokens.expiry_date }); + } catch (error) { + setGoogleDriveError('Failed to complete Google Drive connection'); + } finally { + window.history.replaceState({}, document.title, window.location.pathname); + } + return true; + } + return false; + }; const handleInputChange = e => { const { name, value, type, checked } = e.target; diff --git a/src/components/Shared/Sidebar.jsx b/src/components/Shared/Sidebar.jsx index 69e30c9..065613e 100644 --- a/src/components/Shared/Sidebar.jsx +++ b/src/components/Shared/Sidebar.jsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; +// src/components/Shared/Sidebar.jsx +import React, { useState, useCallback, useRef, useEffect } from 'react'; import { NavLink, Link } from 'react-router-dom'; import styled from 'styled-components'; import { @@ -7,158 +8,114 @@ import { FaCog, FaQuestionCircle, FaCode, - FaChevronLeft, - FaChevronRight, - FaCompressArrowsAlt, + FaBook, } from 'react-icons/fa'; -const STORAGE_KEY = 'cc_sidebar_mini'; - const Sidebar = ({ isOpen, onClose }) => { - const [mini, setMini] = useState(() => { - try { - return localStorage.getItem(STORAGE_KEY) === 'true'; - } catch { - return false; - } - }); + const [isHovered, setIsHovered] = useState(false); + const hoverTimeoutRef = useRef(null); - const firstRender = useRef(true); + const handleNavClick = useCallback(() => { + if (window.innerWidth < 768 && onClose) onClose(); + }, [onClose]); - useEffect(() => { - if (firstRender.current) { - firstRender.current = false; - return; + const handleMouseEnter = useCallback(() => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; } - try { - localStorage.setItem(STORAGE_KEY, mini ? 'true' : 'false'); - } catch {} - }, [mini]); - - const toggleMini = useCallback(() => { - setMini(prev => !prev); + setIsHovered(true); }, []); - const handleResetMini = useCallback(() => { - setMini(false); + const handleMouseLeave = useCallback(() => { + // Small delay to prevent flickering when mouse moves quickly + hoverTimeoutRef.current = setTimeout(() => { + setIsHovered(false); + }, 50); }, []); useEffect(() => { - const handler = (e) => { - const isCmd = e.metaKey || e.ctrlKey; - if (isCmd && (e.key === 'b' || e.key === 'B')) { - e.preventDefault(); - toggleMini(); + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); } }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [toggleMini]); - - const handleNavClick = useCallback(() => { - if (window.innerWidth < 768 && onClose) { - onClose(); - } - }, [onClose]); + }, []); return ( - - + - - - - Code Editor + + Code Editor - - - {mini ? : } - - - + + + + Getting Started + + + + + - Dashboard + Dashboard - - + + - Shared with me + Shared with me - - + + - Settings + Settings - - + + - Help + Help + + - + - Code Editor v1.0.0 + Code Editor v1.0.0 - - - - ); }; -/* Styled components with proper prop filtering */ -const SidebarContainer = styled.div.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +/* Styled components */ +const SidebarContainer = styled.div.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` --full-width: 250px; --mini-width: 72px; - width: ${p => (p.$mini ? 'var(--mini-width)' : 'var(--full-width)')}; - min-width: ${p => (p.$mini ? 'var(--mini-width)' : 'var(--full-width)')}; - transition: width 220ms ease, transform 220ms ease; + width: ${p => (p.$isHovered ? 'var(--full-width)' : 'var(--mini-width)')}; + min-width: ${p => (p.$isHovered ? 'var(--full-width)' : 'var(--mini-width)')}; + transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + will-change: width; display: flex; flex-direction: column; position: fixed; @@ -169,34 +126,24 @@ const SidebarContainer = styled.div.withConfig({ background-color: var(--color-surface); border-right: 1px solid var(--color-border); box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08); - transition: transform 300ms ease-in-out; - transform: translateX(${p => (p.$isOpen ? '0' : '-100%')}); @media (min-width: 768px) { transform: translateX(0); position: static; box-shadow: none; - transition: none; + transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1); } `; -const SidebarBrand = styled.div.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +const SidebarBrand = styled.div.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` padding: 12px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--color-border); justify-content: space-between; - - ${p => - p.$mini && - ` - padding-left: 12px; - padding-right: 8px; - `} + ${p => !p.$isHovered && `padding-left: 12px; padding-right: 8px;`} `; const BrandLink = styled(Link)` @@ -218,48 +165,24 @@ const BrandIcon = styled.span` border-radius: 8px; background: var(--color-surface-light); color: var(--color-primary); - svg { - font-size: 16px; - display: block; - } + svg { font-size: 16px; } `; -const BrandText = styled.span.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +const BrandText = styled.span.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` white-space: nowrap; - - /* show on small screens (mobile) */ - @media (max-width: 767px) { - display: inline; + transition: opacity 120ms ease, transform 120ms ease; + @media (max-width: 767px) { + display: inline; + opacity: 1; + transform: translateX(0); } - - /* hide on desktop so top Navbar remains single brand */ - @media (min-width: 768px) { - display: none; + @media (min-width: 768px) { + opacity: ${p => p.$isHovered ? '1' : '0'}; + transform: ${p => p.$isHovered ? 'translateX(0)' : 'translateX(-10px)'}; + pointer-events: ${p => p.$isHovered ? 'auto' : 'none'}; } `; -const MiniToggle = styled.button` - display: inline-flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 6px; - border: none; - background: transparent; - color: var(--color-text-secondary); - cursor: pointer; - - &:hover { - background: var(--color-surface); - } - - @media (max-width: 720px) { - display: none; - } -`; const SidebarNav = styled.ul` list-style: none; @@ -269,9 +192,7 @@ const SidebarNav = styled.ul` overflow: auto; `; -const NavItem = styled.li.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +const NavItem = styled.li.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` a { display: flex; align-items: center; @@ -283,58 +204,35 @@ const NavItem = styled.li.withConfig({ border-radius: 8px; margin: 4px 8px; transition: background 120ms ease; - - &:hover { - background-color: var(--color-background); - } - + &:hover { background-color: var(--color-background); } &.active { color: var(--color-primary); background-color: var(--color-surface-light); border-left: 3px solid var(--color-primary); padding-left: calc(14px - 3px); - svg { - color: var(--color-primary); - } - } - - svg { - font-size: 16px; - color: var(--color-text-tertiary); - flex-shrink: 0; + svg { color: var(--color-primary); } } + svg { font-size: 16px; color: var(--color-text-tertiary); flex-shrink: 0; } } - - /* center icons when container is mini (desktop only) */ @media (min-width: 768px) { - ${p => - p.$mini && - ` - a { justify-content: center; padding-left: 0; padding-right: 0; } - a svg { margin-right: 0; } - `} + ${p => !p.$isHovered && `a { justify-content: center; padding-left:0; padding-right:0; } a svg { margin-right:0; }`} } `; -const NavText = styled.span.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +const NavText = styled.span.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - - /* if mini on desktop hide text */ - ${p => - p.$mini && - ` - @media (min-width: 768px) { - display: none; - } - `} - - /* always show on small screens */ - @media (max-width: 767px) { - display: inline; + transition: opacity 120ms ease, transform 120ms ease; + @media (max-width: 767px) { + display: inline; + opacity: 1; + transform: translateX(0); + } + @media (min-width: 768px) { + opacity: ${p => p.$isHovered ? '1' : '0'}; + transform: ${p => p.$isHovered ? 'translateX(0)' : 'translateX(-10px)'}; + pointer-events: ${p => p.$isHovered ? 'auto' : 'none'}; } `; @@ -344,22 +242,14 @@ const NavDivider = styled.div` margin: 10px 8px; `; -const SidebarFooter = styled.div.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +const SidebarFooter = styled.div.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px; border-top: 1px solid var(--color-border); - - ${p => - p.$mini && - ` - padding-left: 8px; - padding-right: 8px; - `} + ${p => !p.$isHovered && `padding-left: 8px; padding-right: 8px;`} `; const FooterLeft = styled.div` @@ -368,40 +258,21 @@ const FooterLeft = styled.div` gap: 8px; `; -const FooterText = styled.div.withConfig({ - shouldForwardProp: (prop) => !prop.startsWith('$'), -})` +const FooterText = styled.div.withConfig({ shouldForwardProp: prop => !prop.startsWith('$') })` font-size: 12px; color: var(--color-text-tertiary); - - ${p => - p.$mini && - ` - @media (min-width: 768px) { - display: none; - } - `} - - @media (max-width: 767px) { - display: block; + transition: opacity 120ms ease, transform 120ms ease; + @media (max-width: 767px) { + display: block; + opacity: 1; + transform: translateX(0); } -`; - -const FooterAction = styled.button` - display: inline-flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 6px; - border: none; - background: transparent; - color: var(--color-text-secondary); - cursor: pointer; - - &:hover { - background: var(--color-surface); + @media (min-width: 768px) { + opacity: ${p => p.$isHovered ? '1' : '0'}; + transform: ${p => p.$isHovered ? 'translateX(0)' : 'translateX(-10px)'}; + pointer-events: ${p => p.$isHovered ? 'auto' : 'none'}; } `; -export default Sidebar; \ No newline at end of file + +export default Sidebar; diff --git a/src/pages/GettingStarted.jsx b/src/pages/GettingStarted.jsx new file mode 100644 index 0000000..3b8bc4b --- /dev/null +++ b/src/pages/GettingStarted.jsx @@ -0,0 +1,524 @@ +import { useState, useEffect } from "react"; +import styled from "styled-components"; +import { CheckCircle, Lock, Globe } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { createProject } from "../services/projectService"; +import { googleDriveService } from "../services/googleDriveService"; + +// steps config +const steps = [ + { id: 1, title: "How CodeConclave Works", description: "Learn the basics of how CodeConclave works and explore the features."}, + { id: 2, title: "Connect Google Drive", description: "Connect your Google Drive to save and sync your projects." }, + { id: 3, title: "Create Your Project", description: "Start your first project and explore the editor." }, +]; + +const GettingStarted = () => { + const [currentStep, setCurrentStep] = useState(1); + const [projectName, setProjectName] = useState(""); + const [projectDescription, setProjectDescription] = useState(""); + const [visibility, setVisibility] = useState("private"); + const navigate = useNavigate(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Google Drive state management + const [googleDriveStatus, setGoogleDriveStatus] = useState({ + isConnected: false, + tokenExpiry: null + }); + const [isLoadingGoogleDrive, setIsLoadingGoogleDrive] = useState(false); + const [googleDriveError, setGoogleDriveError] = useState(''); + const [googleDriveSuccess, setGoogleDriveSuccess] = useState(''); + + // Load Google Drive status on component mount + useEffect(() => { + const initializeGoogleDrive = async () => { + // Handle any OAuth callback from Google + await handleGoogleDriveCallback(); + + // Fetch the latest connection status + await loadGoogleDriveStatus(); + }; + + initializeGoogleDrive(); + }, []); + + const loadGoogleDriveStatus = async () => { + try { + setIsLoadingGoogleDrive(true); + const status = await googleDriveService.getConnectionStatus(); + setGoogleDriveStatus(status); + } catch (error) { + setGoogleDriveError('Failed to load Google Drive status'); + } finally { + setIsLoadingGoogleDrive(false); + } + }; + + const handleGoogleDriveConnect = async () => { + setIsLoadingGoogleDrive(true); + setGoogleDriveError(''); + try { + const authUrl = await googleDriveService.getAuthUrl(); + if (authUrl) { + window.location.href = authUrl; + } else { + throw new Error('Did not receive a valid authorization URL.'); + } + } catch (error) { + setGoogleDriveError('Failed to start Google Drive connection.'); + setIsLoadingGoogleDrive(false); + } + }; + + const handleGoogleDriveCallback = async () => { + const urlParams = new URLSearchParams(window.location.search); + const googleDriveStatusParam = urlParams.get('googleDrive'); + const tokensParam = urlParams.get('tokens'); + const authCode = urlParams.get('code'); + + if (googleDriveStatusParam === 'connected' && tokensParam) { + try { + const tokens = JSON.parse(decodeURIComponent(tokensParam)); + await googleDriveService.saveTokens( + tokens.access_token, + tokens.refresh_token, + tokens.expiry_date + ); + + setGoogleDriveSuccess('Google Drive connected successfully!'); + setGoogleDriveStatus({ isConnected: true, tokenExpiry: tokens.expiry_date }); + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + return true; + } catch (error) { + setGoogleDriveError('Failed to save Google Drive tokens'); + } + } else if (googleDriveStatusParam === 'error') { + setGoogleDriveError('Failed to connect to Google Drive'); + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + return true; + } else if (authCode) { + try { + const tokens = await googleDriveService.exchangeCodeForTokens(authCode); + setGoogleDriveSuccess('Google Drive connected successfully!'); + setGoogleDriveStatus({ isConnected: true, tokenExpiry: tokens.expiry_date }); + } catch (error) { + setGoogleDriveError('Failed to complete Google Drive connection'); + } finally { + window.history.replaceState({}, document.title, window.location.pathname); + } + return true; + } + return false; + }; + + const handleNext = async () => { + if (currentStep === 3) { + if (isSubmitting) return; + if (projectName.trim()) { + try { + setIsSubmitting(true); + const projectData = { + name: projectName, + description: projectDescription, + // 👇 Adjust this line to match backend schema + isPublic: visibility === "public", + }; + + await createProject(projectData); + } catch (err) { + console.error("Error creating project:", err); + alert("Failed to create project. Please try again."); + return; + } finally { ++ setIsSubmitting(false); + } + } + navigate("/dashboard"); + } else { + setCurrentStep((s) => s + 1); + } +}; + + + const handleBack = () => { + if (currentStep > 1) setCurrentStep(currentStep - 1); + }; + + return ( + +
+ {steps.map((step) => { + const isCompleted = step.id < currentStep; + const isActive = step.id === currentStep; + return ( + + {step.title} + {isCompleted && } + + ); + })} +
+ + + {currentStep === 1 && ( + +

{steps[0].title}

+

{steps[0].description}

+
+ )} + + {currentStep === 2 && ( + +

{steps[1].title}

+

{steps[1].description}

+ + {googleDriveError && {googleDriveError}} + {googleDriveSuccess && {googleDriveSuccess}} + + + +

CodeConclave Storage

+

Your default workspace to store projects.

+ Available Space: 5GB +
+ +

Google Drive

+

Sync projects with your Google Drive.

+ + {googleDriveStatus.isConnected ? 'Connected' : 'Not Connected'} + + + {isLoadingGoogleDrive ? 'Connecting...' : 'Connect Google Drive'} + +
+
+
+ )} + + {currentStep === 3 && ( + +

{steps[2].title}

+

{steps[2].description}

+ + {/* Project Name */} + + + setProjectName(e.target.value)} + /> + + + {/* Description */} + + +