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
10 changes: 10 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"jwt-decode": "^4.0.0",
"katex": "^0.16.22",
"lodash": "^4.17.21",
"lucide-react": "^0.548.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.7.0",
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import AuthSuccess from './pages/AuthSuccess';

import ProtectedRoute from './components/ProtectedRoute';
import { AuthProvider } from './context/AuthContext';
import ThemeToggle from './components/ThemeToggle';

function App() {
return (
<AuthProvider>
<Routes>
<Route path="/auth-success" element={<AuthSuccess />} />

<div className="fixed top-4 right-4 z-50">
<ThemeToggle />
</div>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/" element={
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// src/App.jsx
import React from "react";
import ThemeToggle from "./components/ThemeToggle";

export default function App() {
return (
<div className="min-h-screen">
<header className="flex items-center justify-between p-4 border-b" style={{ borderColor: "rgba(0,0,0,0.06)" }}>
<h1 className="text-xl font-semibold">CollabSpace</h1>
<div className="flex items-center gap-3">
{/* place ThemeToggle where appropriate in your toolbar/header */}
<ThemeToggle />
</div>
</header>

<main className="p-6">
<div className="card rounded-lg shadow-sm p-6">
<h2 className="text-lg font-medium mb-2">Welcome to CollabSpace</h2>
<p className="text-sm text-muted-foreground">Your content goes here. The theme toggle above switches light/dark modes.</p>
</div>
</main>
</div>
);
}
27 changes: 27 additions & 0 deletions frontend/src/components/ProtectedRoute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { Navigate } from "react-router-dom";

export default function ProtectedRoute({ children }) {
const isDev =
(typeof import !== "undefined" &&
typeof import.meta !== "undefined" &&
import.meta.env &&
import.meta.env.MODE === "development") ||
process.env.NODE_ENV === "development";

if (isDev) {
return children; // bypass auth in dev
}

// existing production check — edit the key to match your app
const token =
localStorage.getItem("token") ||
localStorage.getItem("authToken") ||
sessionStorage.getItem("token");

if (!token) {
return <Navigate to="/login" replace />;
}

return children;
}
71 changes: 71 additions & 0 deletions frontend/src/components/ThemeToggle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";

export default function ThemeToggle() {
const [darkMode, setDarkMode] = useState(() => localStorage.getItem("theme") === "dark");

useEffect(() => {
if (darkMode) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}, [darkMode]);

return (
<div
style={{
position: "fixed",
top: 12,
right: 24,
zIndex: 50
}}
>
<button
type="button"
onClick={() => setDarkMode(v => !v)}
aria-label={`Switch to ${darkMode ? "light" : "dark"} mode`}
title={`Switch to ${darkMode ? "light" : "dark"} mode`}
// force size smaller (56x56); adjust here if needed
style={{
height: 56,
width: 56,
fontSize: 32,
lineHeight: 1,
borderWidth: 1,
borderStyle: "solid",
// match the dark frame to page bg; keep light frame slightly elevated
borderColor: darkMode ? "#0b0f19" : "rgb(203,213,225)",
backgroundColor: darkMode ? "transparent" : "rgba(255,255,255,0.9)",
borderRadius: 12,
boxShadow: "0 4px 12px rgba(0,0,0,0.25)",
display: "grid",
placeItems: "center",
backdropFilter: "blur(6px)",
transition: "background-color 150ms ease, border-color 150ms ease"
}}
onMouseEnter={(e) => {
if (darkMode) {
e.currentTarget.style.backgroundColor = "rgba(30,41,59,0.6)";
} else {
e.currentTarget.style.backgroundColor = "rgb(241,245,249)";
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = darkMode
? "transparent"
: "rgba(255,255,255,0.9)";
}}
>
{darkMode ? (
<span style={{ userSelect: "none" }}>☀️</span>
) : (
<span style={{ userSelect: "none" }}>🌙</span>
)}
</button>
</div>
);

}
71 changes: 71 additions & 0 deletions frontend/src/hooks/useTheme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// src/hooks/useTheme.js
import { useEffect, useState, useCallback } from "react";

const STORAGE_KEY = "collabspace:theme"; // persist key

function getPreferredTheme() {
// 1) explicit saved value
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === "dark" || saved === "light") return saved;
} catch (e) {
// ignore
}

// 2) system preference
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}

// default
return "light";
}

export default function useTheme() {
const [theme, setThemeState] = useState(() => getPreferredTheme());

const applyTheme = useCallback((t) => {
const root = document.documentElement;
if (!root) return;
if (t === "dark") root.classList.add("dark");
else root.classList.remove("dark");
}, []);

useEffect(() => {
applyTheme(theme);
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch (e) {}
}, [theme, applyTheme]);

// observe system preference changes so the app can respond
useEffect(() => {
const mq = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
if (!mq) return;
const handle = (e) => {
// only change if user hasn't explicitly saved a preference
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved !== null) return;
} catch (err) {}
setThemeState(e.matches ? "dark" : "light");
};
if (mq.addEventListener) mq.addEventListener("change", handle);
else mq.addListener(handle);
return () => {
if (mq.removeEventListener) mq.removeEventListener("change", handle);
else mq.removeListener(handle);
};
}, []);

const setTheme = useCallback((t) => {
if (t !== "dark" && t !== "light") return;
setThemeState(t);
}, []);

const toggle = useCallback(() => {
setThemeState((prev) => (prev === "dark" ? "light" : "dark"));
}, []);

return { theme, setTheme, toggle };
}
41 changes: 41 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,44 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Define light theme variables on :root and dark overrides under .dark */
:root {
--bg: #ffffff;
--panel: #f8fafc;
--muted: #6b7280;
--text: #0f172a;
--primary: #4f46e5;
}

/* When ".dark" is applied to <html> or <body>, override */
.dark {
--bg: #0b1220;
--panel: #071127;
--muted: #94a3b8;
--text: #e6eef8;
--primary: #7c5cff;
}

/* Example utility classes using variables */
body {
background-color: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

/*make panels/cards automatically switch */
.card {
background-color: var(--panel);
color: var(--text);
}

/* small helper for the theme toggle button */
.theme-toggle {
@apply inline-flex items-center justify-center rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-offset-1;
}
34 changes: 32 additions & 2 deletions frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// import ReactDOM from 'react-dom/client';
// import './index.css';
// import App from './App';
import reportWebVitals from './reportWebVitals';


// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
Expand All @@ -13,10 +13,37 @@ import reportWebVitals from './reportWebVitals';


// src/main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

// Dark-mode initialization (runs before React renders) ---
(function initTheme() {
try {
const STORAGE_KEY = 'collabspace:theme';
const saved = localStorage.getItem(STORAGE_KEY);
const prefersDark =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme =
saved === 'dark' || saved === 'light'
? saved
: prefersDark
? 'dark'
: 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch (e) {
console.error('Theme init error', e);
}
})();

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
Expand All @@ -26,7 +53,10 @@ ReactDOM.createRoot(document.getElementById('root')).render(
</React.StrictMode>
);

// CRA performance helper (optional)
reportWebVitals();

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

23 changes: 23 additions & 0 deletions frontend/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";

// Immediately apply the saved or preferred theme before React paints
(function initTheme() {
try {
const STORAGE_KEY = "collabspace:theme";
const saved = localStorage.getItem(STORAGE_KEY);
const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
const theme = saved === "dark" || saved === "light" ? saved : (prefersDark ? "dark" : "light");
if (theme === "dark") document.documentElement.classList.add("dark");
else document.documentElement.classList.remove("dark");
} catch (e) {
// ignore
}
})();

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);
Loading