Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
# ============================================
# EquoraScale MVP - Environment Variables
# ============================================

# ============================================
# AI Services Configuration
# ============================================

# Backend API Base URL
VITE_API_BASE_URL=http://localhost:3000

#OpenRouter API Key
VITE_API_KEY=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
39 changes: 39 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ============================================
# EquoraScale MVP - Environment Variables
# ============================================
# Copy this file to .env and fill in your actual values
# Never commit your .env file to version control!

# ============================================
# AI Services Configuration
# ============================================

# OpenRouter API Key
# Used for AI-powered document analysis and Q&A features
# Get your API key from: https://openrouter.ai/keys
# Required for: Document question answering (askDocumentQuestion function)
VITE_API_KEY=your_openrouter_api_key_here

# Google Gemini API Key (Optional)
# Currently not in use, but reserved for future Gemini AI integration
# Get your API key from: https://aistudio.google.com/app/apikey
# Uncomment when needed:
# VITE_GEMINI_API_KEY=your_gemini_api_key_here

# ============================================
# Backend API Configuration
# ============================================

# Backend API Base URL
# Default: http://localhost:3000
# Change this to your production backend URL when deploying
# Example: https://api.equorascale.com
VITE_API_BASE_URL=http://localhost:3000

# ============================================
# Notes
# ============================================
# - All VITE_ prefixed variables are exposed to the client-side code
# - Never commit sensitive keys to version control
# - For production, set these in your hosting platform's environment variables
# - The app will work without VITE_API_KEY but AI features will be limited
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ dist
dist-ssr
*.local

# dotenv environment variable files
.env
.env.local
.env.development
.env.production
.env.test
.env.test.local
.env.production.local
.env.development.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
239 changes: 171 additions & 68 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, createContext, useContext } from 'react';
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import { User } from './types';
import LandingPage from './components/Landing/LandingPage';
Expand All @@ -9,6 +9,27 @@ import DashboardLayout from './components/Layout/DashboardLayout';
import RepositoryView from './components/Dashboard/RepositoryView';
import { getProfile, loginUser, logoutUser } from './services/auth';
import SettingsPage from './components/Settings/SettingsPage';
import AdminRoute from './components/Admin/AdminRoute';
import RoleRoute from './components/Admin/RoleRoute';
import AdminDashboard from './components/Admin/AdminDashboard';
import UserManagement from './components/Admin/UserManagement';
import AdminAnalytics from './components/Admin/AdminAnalytics';
import DesktopRequired from './components/UI/DesktopRequired';

export type ThemeMode = 'light' | 'dark' | 'system';

// --- Theme Context ---
export const ThemeContext = createContext<{
theme: ThemeMode;
setTheme: (theme: ThemeMode) => void;
isDarkMode: boolean;
}>({
theme: 'system',
setTheme: () => {},
isDarkMode: false,
});

export const useTheme = () => useContext(ThemeContext);

// --- Auth Context Mockup for App-wide state ---
export const AuthContext = React.createContext<{
Expand All @@ -32,29 +53,88 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
};

const PublicLayout = () => (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 transition-colors duration-300">
<div className="min-h-screen bg-slate-50 dark:bg-slate-950">
<Outlet />
</div>
);

const App: React.FC = () => {
const [isDesktop, setIsDesktop] = useState(() => {
if (typeof window === 'undefined') return true;
return window.innerWidth >= 1024;
});

const [user, setUser] = useState<User | null>(() => {
return JSON.parse(localStorage.getItem('eqorascale_user') || 'null');
});
const [authLoading, setAuthLoading] = useState(true);

const [isDarkMode, setIsDarkMode] = useState(() => {
// Theme state management
const [theme, setTheme] = useState<ThemeMode>(() => {
const saved = localStorage.getItem('theme');
if (saved) return saved === 'dark';
return (saved as ThemeMode) || 'system';
});

// Calculate actual dark mode based on theme preference
const getSystemDarkMode = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};

const [isDarkMode, setIsDarkMode] = useState(() => {
if (theme === 'system') {
return getSystemDarkMode();
}
return theme === 'dark';
});

// Apply theme changes
useEffect(() => {
const root = window.document.documentElement;
if (isDarkMode) root.classList.add('dark');
else root.classList.remove('dark');
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
let shouldBeDark: boolean;

if (theme === 'system') {
shouldBeDark = getSystemDarkMode();
} else {
shouldBeDark = theme === 'dark';
}

setIsDarkMode(shouldBeDark);

if (shouldBeDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}

localStorage.setItem('theme', theme);
}, [theme]);

// Listen to system theme changes when theme is set to 'system'
useEffect(() => {
if (theme !== 'system') return;

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const root = window.document.documentElement;
if (e.matches) {
root.classList.add('dark');
setIsDarkMode(true);
} else {
root.classList.remove('dark');
setIsDarkMode(false);
}
};

// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
} else {
// Fallback for older browsers
mediaQuery.addListener(handleChange);
return () => mediaQuery.removeListener(handleChange);
}
}, [theme]);

useEffect(() => {
const token = localStorage.getItem('eqorascale_token');
Expand Down Expand Up @@ -88,69 +168,92 @@ const App: React.FC = () => {
localStorage.removeItem('eqorascale_user');
};

// Show desktop required message for mobile/tablet
if (!isDesktop) {
return <DesktopRequired />;
}

return (
<AuthContext.Provider value={{ user, login, logout, loading: authLoading }}>
<ToastProvider>
<BrowserRouter>
<Routes>
{/* Public Section */}
<Route element={<PublicLayout />}>
<Route path="/" element={<LandingPage />} />
</Route>

{/* Auth Section */}
<Route path="/login" element={
user ? <Navigate to="/app" replace /> : <AuthForm onLogin={login} />
} />
<Route path="/signup" element={
user ? <Navigate to="/app" replace /> : <AuthForm onLogin={login} />
} />

{/* Dashboard Section */}
<Route
path="/app"
element={
<ProtectedRoute>
<DashboardLayout
user={user}
onLogout={logout}
isDarkMode={isDarkMode}
toggleTheme={() => setIsDarkMode(!isDarkMode)}
/>
</ProtectedRoute>
}
>
<Route index element={<Navigate to="repository/ALL" replace />} />
<Route path="repository/:tab" element={<RepositoryView />} />

{/* Scale placeholders */}
<Route path="collections" element={
<div className="p-8 flex items-center justify-center h-full">
<div className="text-center opacity-40">
<p className="text-4xl font-black mb-2 uppercase tracking-widest">Collections</p>
<p className="text-sm font-bold uppercase tracking-widest">Module coming soon</p>
</div>
</div>
} />
<Route path="analytics" element={
<div className="p-8 flex items-center justify-center h-full">
<div className="text-center opacity-40">
<p className="text-4xl font-black mb-2 uppercase tracking-widest">Analytics</p>
<p className="text-sm font-bold uppercase tracking-widest">Module coming soon</p>
</div>
</div>
<ThemeContext.Provider value={{ theme, setTheme, isDarkMode }}>
<AuthContext.Provider value={{ user, login, logout, loading: authLoading }}>
<ToastProvider>
<BrowserRouter>
<Routes>
{/* Public Section */}
<Route element={<PublicLayout />}>
<Route path="/" element={<LandingPage />} />
</Route>

{/* Auth Section */}
<Route path="/login" element={
user ? <Navigate to="/app" replace /> : <AuthForm onLogin={login} />
} />
<Route path="settings" element={
<SettingsPage />
<Route path="/signup" element={
user ? <Navigate to="/app" replace /> : <AuthForm onLogin={login} />
} />
</Route>

{/* Catch-all */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
</ToastProvider>
</AuthContext.Provider>

{/* Dashboard Section */}
<Route
path="/app"
element={
<ProtectedRoute>
<DashboardLayout
user={user}
onLogout={logout}
isDarkMode={isDarkMode}
/>
</ProtectedRoute>
}
>
<Route index element={<Navigate to="repository/ALL" replace />} />
<Route path="repository/:tab" element={<RepositoryView />} />

{/* Collections - All authenticated users */}
<Route path="collections" element={
<div className="p-8 flex items-center justify-center h-full">
<div className="text-center opacity-40">
<p className="text-4xl font-black mb-2 uppercase tracking-widest">Collections</p>
<p className="text-sm font-bold uppercase tracking-widest">Module coming soon</p>
</div>
</div>
} />

{/* Analytics - Admin only */}
<Route path="analytics" element={
<AdminRoute>
<div className="p-8 flex items-center justify-center h-full">
<div className="text-center opacity-40">
<p className="text-4xl font-black mb-2 uppercase tracking-widest">Analytics</p>
<p className="text-sm font-bold uppercase tracking-widest">Module coming soon</p>
</div>
</div>
</AdminRoute>
} />

{/* Settings - All authenticated users (but admin sees more) */}
<Route path="settings" element={
<SettingsPage />
} />

{/* Admin Panel Routes */}
<Route path="admin" element={
<AdminRoute>
<Outlet />
</AdminRoute>
}>
<Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<AdminDashboard />} />
<Route path="users" element={<UserManagement />} />
</Route>
</Route>

{/* Catch-all */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
</ToastProvider>
</AuthContext.Provider>
</ThemeContext.Provider>
);
};

Expand Down
Loading