Spaces:
Sleeping
Sleeping
| import { useState, useRef, useEffect } from 'react' | |
| import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate, Link } from 'react-router-dom' | |
| import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarRightCollapse } from "react-icons/tb"; | |
| import { AiFillHome } from "react-icons/ai"; | |
| import { BiPodcast } from "react-icons/bi"; | |
| import { FaMicrophoneAlt } from "react-icons/fa"; | |
| import { MdDarkMode, MdLightMode } from "react-icons/md"; | |
| import { ImPodcast } from "react-icons/im"; | |
| import { RiChatVoiceAiFill } from "react-icons/ri"; | |
| import { FaUser, FaSignOutAlt } from "react-icons/fa"; | |
| import { PiGooglePodcastsLogo } from "react-icons/pi"; | |
| import { TiFlowSwitch } from "react-icons/ti"; | |
| import { SiNodemon } from "react-icons/si"; | |
| import React from 'react'; | |
| import Home from './pages/Home' | |
| import Podcasts from './pages/Podcasts' | |
| import Workflows from './pages/Workflows' | |
| import Demo from './pages/Demo' | |
| import WorkflowEditor from './components/WorkflowEditor' | |
| import UserModal from './components/UserModal' | |
| import Toast from './components/Toast' | |
| import './App.css' | |
| // Global toast context | |
| export const ToastContext = React.createContext({ | |
| toast: null, | |
| setToast: () => { } | |
| }); | |
| function App() { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [isDark, setIsDark] = useState(true); | |
| const [isLogin, setIsLogin] = useState(true); | |
| const [isAuthenticated, setIsAuthenticated] = useState(false); | |
| const [isModalOpen, setIsModalOpen] = useState(false); | |
| const [toast, setToast] = useState(null); | |
| const sidebarRef = useRef(null); | |
| // Check for token on initial load | |
| useEffect(() => { | |
| const token = localStorage.getItem('token'); | |
| if (token) { | |
| // Validate token by making a request to the backend | |
| const validateToken = async () => { | |
| try { | |
| const response = await fetch('http://localhost:8000/user/me', { | |
| headers: { | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }); | |
| if (response.ok) { | |
| setIsAuthenticated(true); | |
| console.log('User authenticated from stored token'); | |
| } else { | |
| // Token is invalid, remove it | |
| localStorage.removeItem('token'); | |
| setIsAuthenticated(false); | |
| console.log('Stored token is invalid, removed'); | |
| } | |
| } catch (error) { | |
| console.error('Error validating token:', error); | |
| // Don't remove token on network errors to allow offline access | |
| } | |
| }; | |
| validateToken(); | |
| } | |
| }, []); | |
| useEffect(() => { | |
| const handleClickOutside = (event) => { | |
| if (sidebarRef.current && !sidebarRef.current.contains(event.target)) { | |
| setIsOpen(false); | |
| } | |
| }; | |
| document.addEventListener('mousedown', handleClickOutside); | |
| return () => { | |
| document.removeEventListener('mousedown', handleClickOutside); | |
| }; | |
| }, []); | |
| const toggleSidebar = () => { | |
| setIsOpen(!isOpen); | |
| }; | |
| const toggleTheme = (e) => { | |
| e.preventDefault(); | |
| setIsDark(!isDark); | |
| document.body.style.backgroundColor = !isDark ? '#040511' : '#ffffff'; | |
| document.body.style.color = !isDark ? '#ffffff' : '#000000'; | |
| }; | |
| const toggleForm = () => { | |
| setIsLogin(!isLogin); | |
| }; | |
| const handleLogout = () => { | |
| localStorage.removeItem('token'); | |
| setIsAuthenticated(false); | |
| showToast('Logged out successfully', 'success'); | |
| }; | |
| const showToast = (message, type = 'success') => { | |
| setToast({ message, type }); | |
| }; | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const username = formData.get('username'); | |
| const password = formData.get('password'); | |
| try { | |
| const response = await fetch(`http://localhost:8000/${isLogin ? 'login' : 'signup'}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ username, password }), | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| localStorage.setItem('token', data.access_token); | |
| setIsAuthenticated(true); | |
| showToast(`Successfully ${isLogin ? 'logged in' : 'signed up'}!`, 'success'); | |
| } else { | |
| const error = await response.json(); | |
| showToast(error.detail, 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| showToast('An error occurred during authentication', 'error'); | |
| } | |
| }; | |
| return ( | |
| <ToastContext.Provider value={{ toast, setToast }}> | |
| <Router> | |
| <> | |
| <div className={`${isAuthenticated && 'simple-bg'}`}> | |
| </div> | |
| <div className={`app-container ${isDark ? 'dark' : 'light'} ${!isAuthenticated && isDark ? 'bg-login' : ''} `} > | |
| <nav ref={sidebarRef} className={`sidebar ${isOpen ? 'open' : 'closed'}`}> | |
| <div className='product-name'> | |
| <span><PiGooglePodcastsLogo /> PodCraft <sup>©</sup> <sub>Beta</sub></span> | |
| </div> | |
| <span className="toggle-btn" onClick={toggleSidebar}> | |
| {isOpen ? <TbLayoutSidebarLeftCollapse /> : <TbLayoutSidebarRightCollapse />} | |
| </span> | |
| <div className="nav-links"> | |
| {isAuthenticated && ( | |
| <> | |
| <Link to="/home" className="nav-link"> | |
| <AiFillHome /> | |
| <span className={`link-text ${!isOpen && 'hidden'}`}>Home</span> | |
| </Link> | |
| <Link to="/podcasts" className="nav-link"> | |
| <BiPodcast /> | |
| <span className={`link-text ${!isOpen && 'hidden'}`}>Podcasts</span> | |
| </Link> | |
| <Link to="/workflows" className="nav-link"> | |
| <TiFlowSwitch /> | |
| <span className={`link-text ${!isOpen && 'hidden'}`}>Workflows</span> | |
| </Link> | |
| <Link to="/demo" className="nav-link"> | |
| <SiNodemon /> | |
| <span className={`link-text ${!isOpen && 'hidden'}`}>Demo</span> | |
| </Link> | |
| <div className="nav-divider"></div> | |
| <a href="#" className="nav-link" onClick={() => setIsModalOpen(true)}> | |
| <FaUser /> | |
| <span className={`link-text ${!isOpen && 'hidden'}`}>Profile</span> | |
| </a> | |
| <a href="#" className="nav-link" onClick={handleLogout}> | |
| <FaSignOutAlt /> | |
| <span className={`link-text ${!isOpen && 'hidden'}`}>Logout</span> | |
| </a> | |
| </> | |
| )} | |
| <a href="#" className="nav-link theme-toggle" onClick={toggleTheme}> | |
| {isDark ? <MdDarkMode /> : <MdLightMode />} | |
| <span className={`link-text ${!isOpen && 'hidden'}`}> | |
| {isDark ? 'Dark Mode' : 'Light Mode'} | |
| </span> | |
| </a> | |
| </div> | |
| </nav> | |
| <main className={`main-content ${!isOpen ? 'expanded' : ''}`}> | |
| {!isAuthenticated ? ( | |
| <div className="auth-container"> | |
| <div className="hero-section"> | |
| <div className="hero-content"> | |
| <div className="hero-logo"> | |
| <ImPodcast /> | |
| <h1>PodCraft</h1> | |
| </div> | |
| <p className="hero-tagline">One prompt <RiChatVoiceAiFill /> to Podcast <BiPodcast /></p> | |
| </div> | |
| </div> | |
| <div className="auth-form-container"> | |
| <h2>{isLogin ? 'Login' : 'Sign Up'}</h2> | |
| <form className="auth-form" onSubmit={handleSubmit}> | |
| <div className="form-group"> | |
| <input type="text" name="username" placeholder="Username" required /> | |
| </div> | |
| <div className="form-group"> | |
| <input type="password" name="password" placeholder="Password" required /> | |
| </div> | |
| <button type="submit" className="submit-btn"> | |
| {isLogin ? 'Login' : 'Sign Up'} | |
| </button> | |
| </form> | |
| <p className="form-switch"> | |
| {isLogin ? "Don't have an account? " : "Already have an account? "} | |
| <a href="#" onClick={toggleForm}> | |
| {isLogin ? 'Sign Up' : 'Login'} | |
| </a> | |
| </p> | |
| </div> | |
| </div> | |
| ) : ( | |
| <Routes> | |
| <Route path="/home" element={<Home />} /> | |
| <Route path="/podcasts" element={<Podcasts />} /> | |
| <Route path="/workflows" element={<Workflows />} /> | |
| <Route path="/demo" element={<Demo />} /> | |
| <Route path="/workflows/workflow/:workflowId" element={<WorkflowEditor />} /> | |
| <Route path="/" element={<Navigate to="/home" replace />} /> | |
| </Routes> | |
| )} | |
| </main> | |
| <UserModal | |
| isOpen={isModalOpen} | |
| onClose={() => setIsModalOpen(false)} | |
| token={localStorage.getItem('token')} | |
| /> | |
| <div className="toast-container"> | |
| {toast && ( | |
| <Toast | |
| message={toast.message} | |
| type={toast.type} | |
| onClose={() => setToast(null)} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| </> | |
| </Router> | |
| </ToastContext.Provider> | |
| ) | |
| } | |
| export default App | |