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
13 changes: 12 additions & 1 deletion client/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<script>
const theme = localStorage.getItem('theme');
if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password Manager</title>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<body class="bg-white dark:bg-gray-950 text-black dark:text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
1,649 changes: 1,272 additions & 377 deletions client/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@clerk/clerk-react": "^5.33.0",
"@clerk/themes": "^2.4.4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
Expand All @@ -23,13 +24,17 @@
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@tailwindcss/postcss": "^4.1.11",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react-swc": "^3.10.2",
"autoprefixer": "^10.4.21",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
Expand Down
6 changes: 6 additions & 0 deletions client/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {}
}
}
10 changes: 9 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { useEffect } from 'react';
import { Box } from '@mui/material';
import Navbar from './components/Navbar';
import { Routes, Route, Outlet } from 'react-router-dom';
import Folders from './pages/Folders';
import Footer from './components/Footer';
import { applyTheme, getInitialTheme } from './theme';

const App = () => {
// TODO: Use `isLoaded` - Handle loading state to avoid flickering and show a loading spinner or message
useEffect(() => {
const theme = getInitialTheme();
applyTheme(theme);
}, []);

return (
<Box
className="min-h-screen bg-white dark:bg-gray-900 text-black dark:text-white"
sx={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
width: '100%',
}}
>
<Navbar />
Expand Down
70 changes: 70 additions & 0 deletions client/src/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createContext, useState, useEffect, useContext } from 'react';
import { applyTheme, getInitialTheme } from './theme';
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { useAuth } from '@clerk/clerk-react';

import type { ReactNode } from 'react';
import type { Theme } from './theme';

const ThemeContext = createContext<{
theme: Theme;
toggleTheme: () => void;
}>({
theme: 'light',
toggleTheme: () => {},
});

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

const getMuiTheme = (mode: Theme) =>
createTheme({
palette: {
mode,
primary: {
main: mode === 'dark' ? '#60a5fa' : '#1d4ed8',
},
background: {
default: mode === 'dark' ? '#111827' : '#ffffff',
paper: mode === 'dark' ? '#1f2937' : '#f9fafb',
},
text: {
primary: mode === 'dark' ? '#ffffff' : '#000000',
},
},
});

export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const { isSignedIn, isLoaded } = useAuth();
const [theme, setTheme] = useState<Theme>('light');

useEffect(() => {
if (!isLoaded) return;

const stored = getInitialTheme();

if (isSignedIn) {
setTheme(stored);
applyTheme(stored);
} else {
setTheme('light');
applyTheme('light');
}
}, [isSignedIn, isLoaded]);

const toggleTheme = () => {
const newTheme: Theme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);

if (isSignedIn) {
applyTheme(newTheme);
}
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<MuiThemeProvider theme={getMuiTheme(theme)}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
);
};
23 changes: 17 additions & 6 deletions client/src/components/Folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,34 @@ const Folder = ({
}) => {
return (
<Box
sx={{
sx={(theme) => ({
width: '16rem',
height: '8rem',
border: '1px solid #e0e0e0',
borderRadius: '1rem',
padding: '1.25rem',
backgroundColor: '#fff',
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.04)',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
border: `1px solid ${
theme.palette.mode === 'light'
? '#e0e0e0'
: theme.palette.divider
}`,
boxShadow:
theme.palette.mode === 'light'
? '0 2px 6px rgba(0, 0, 0, 0.04)'
: '0 2px 6px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s ease-in-out',
cursor: 'pointer',
display: 'flex',
gap: '1rem',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
boxShadow:
theme.palette.mode === 'light'
? '0 4px 12px rgba(0, 0, 0, 0.08)'
: '0 4px 12px rgba(0, 0, 0, 0.4)',
transform: 'translateY(-2px)',
},
}}
})}
>
{/* Icon on the left */}
<Box mt="4px">
Expand Down
21 changes: 16 additions & 5 deletions client/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ const Footer = () => {
return (
<Box
component="footer"
className="bg-white dark:bg-gray-900 text-black dark:text-white border-t border-gray-200 dark:border-gray-700"
sx={{
mt: 'auto',
borderTop: '1px solid #e0e0e0',
padding: {
xs: '1.5rem',
sm: '2rem 3rem',
Expand All @@ -24,22 +24,33 @@ const Footer = () => {
>
<Typography
variant="body1"
sx={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}
className="flex items-center gap-2"
>
<Lock />{' '}
Password Manager
</Typography>

<Stack direction="row" spacing={3}>
<Link href="https://github.com/Code-A2Z/password-manager" underline="hover" color="text.primary">
<Link
href="https://github.com/Code-A2Z/password-manager"
underline="hover"
className="text-black dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
>
Repository
</Link>
<Link href="https://discord.gg/tSqtvHUJzE" underline="hover" color="text.primary">
<Link
href="https://discord.gg/tSqtvHUJzE"
underline="hover"
className="text-black dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
>
Discord
</Link>
</Stack>

<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400 text-center sm:text-right"
>
© {new Date().getFullYear()} Code A2Z.
<br />
All rights reserved.
Expand Down
22 changes: 16 additions & 6 deletions client/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Box, Typography } from '@mui/material';
import { Box, Typography, IconButton } from '@mui/material';
import { UserButton } from '@clerk/clerk-react';
import { Folder, Lock, Moon } from 'lucide-react';
import { dark } from '@clerk/themes';
import { Folder, Lock, Moon, Sun } from 'lucide-react';
import { useThemeContext } from '../ThemeContext';

const Navbar = () => {
const { theme, toggleTheme } = useThemeContext();
return (
<Box
component="nav"
className="bg-white dark:bg-gray-900 text-black dark:text-white"
sx={{
borderBottom: '1px solid #e0e0e0',
padding: {
Expand All @@ -32,8 +36,7 @@ const Navbar = () => {
gap: '8px',
}}
>
<Lock />{' '}
Password Manager
<Lock /> Password Manager
</Typography>
<Box
sx={{
Expand All @@ -43,8 +46,15 @@ const Navbar = () => {
}}
>
<Folder />
<Moon />
<UserButton />
<IconButton onClick={toggleTheme} sx={{ color: 'text.primary' }}>
{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
</IconButton>
<UserButton
key={theme}
appearance={{
baseTheme: theme === 'dark' ? dark : undefined,
}}
/>
</Box>
</Box>
</Box>
Expand Down
28 changes: 28 additions & 0 deletions client/src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react";

export default function ThemeToggle() {
const [theme, setTheme] = useState(localStorage.getItem("theme") || "light");

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

const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};

return (
<button
onClick={toggleTheme}
className="p-2 rounded-md bg-gray-200 dark:bg-gray-700 text-black dark:text-white"
>
{theme === "dark" ? "🌙 Dark" : "☀️ Light"}
</button>
);
}
18 changes: 15 additions & 3 deletions client/src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
body {
font-family: 'Sora', sans-serif;
@tailwind base;
@tailwind components;
@tailwind utilities;

html, body, #root {
height: 100%;
width: 100%;
margin: 0;
}
padding: 0;
font-family: 'Sora', sans-serif;
}

html.dark body {
background-color: #111827;
color: white;
}
Loading