diff --git a/src/App.tsx b/src/App.tsx index f936c6e..ba8eb72 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,10 @@ import React from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { Login } from './pages/Login'; import { Profile } from './pages/Profile'; +import { Settings } from './pages/Settings'; import { useStore } from './store/useStore'; +import { ThemeProvider } from './components/ThemeProvider'; +import { Layout } from './components/Layout'; import './styles/globals.css'; const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -10,25 +13,66 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) = if (!apiKey) { return ; } - return <>{children}; + return {children}; }; +const PlaceholderPage: React.FC<{ title: string }> = ({ title }) => ( +
+

{title}

+

Coming soon...

+
+); + function App() { return ( - - - } /> - - - - } - /> - } /> - - + + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + + ); } diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..cbda0ee --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Navigation } from './Navigation'; + +export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( +
+ +
+ {children} +
+
+ ); +}; diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx new file mode 100644 index 0000000..5395601 --- /dev/null +++ b/src/components/Navigation.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { User, Calendar, GraduationCap, MessageSquare, Settings } from 'lucide-react'; + +const navItems = [ + { path: '/profile', label: 'Profile', icon: User }, + { path: '/schedule', label: 'Schedule', icon: Calendar }, + { path: '/grades', label: 'Grades', icon: GraduationCap }, + { path: '/messages', label: 'Messages', icon: MessageSquare }, + { path: '/settings', label: 'Settings', icon: Settings }, +]; + +export const Navigation: React.FC = () => { + return ( + <> + + + + + ); +}; diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..1721e21 --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -0,0 +1,28 @@ +import React, { useEffect } from 'react'; +import { useStore } from '../store/useStore'; + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { theme, seedColor } = useStore(); + + useEffect(() => { + const root = document.documentElement; + + // Set Theme Class + if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + root.classList.add('dark'); + root.style.colorScheme = 'dark'; + } else { + root.classList.remove('dark'); + root.style.colorScheme = 'light'; + } + + // Set Primary Color (Seed) + root.style.setProperty('--md-sys-color-primary-base', seedColor); + + // Simplification: In a real app we'd use material-color-utilities here. + // For now we just use the seed color as primary. + root.style.setProperty('--md-sys-color-primary', seedColor); + }, [theme, seedColor]); + + return <>{children}; +}; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index acd3e3f..ee00b0c 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,15 +1,13 @@ import React, { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { Profile as ProfileType } from '../types/api'; -import { useStore } from '../store/useStore'; -import { useNavigate } from 'react-router-dom'; -import { LogOut, User, Mail, GraduationCap, Users } from 'lucide-react'; +import { useNavigate, Link } from 'react-router-dom'; +import { User, Mail, GraduationCap, Users, Calendar, GraduationCap as GradesIcon, MessageSquare, Settings } from 'lucide-react'; export const Profile: React.FC = () => { const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const { reset } = useStore(); const navigate = useNavigate(); useEffect(() => { @@ -30,11 +28,6 @@ export const Profile: React.FC = () => { fetchProfile(); }, [navigate]); - const handleLogout = () => { - reset(); - navigate('/login'); - }; - if (loading) return
Loading profile...
; if (error) return (
@@ -48,18 +41,14 @@ export const Profile: React.FC = () => { return (
-
+

Profile

-
{profile && (
- +
Full Name
{profile.fullName}
@@ -68,7 +57,7 @@ export const Profile: React.FC = () => { {profile.email && (
- +
Email
{profile.email}
@@ -78,7 +67,7 @@ export const Profile: React.FC = () => { {profile.group && (
- +
Group
{profile.group}
@@ -88,7 +77,7 @@ export const Profile: React.FC = () => { {profile.faculty && (
- +
Faculty
{profile.faculty}
@@ -99,12 +88,24 @@ export const Profile: React.FC = () => { )}
-

Other Services

+

Quick Access

- - - - + + + Schedule + + + + Grades + + + + Messages + + + + Settings +
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..86b476b --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { useStore } from '../store/useStore'; +import { Palette, Moon, Sun, Monitor, LogOut } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; + +const colors = [ + { name: 'Blue', value: '#0061a4' }, + { name: 'Red', value: '#ba1a1a' }, + { name: 'Green', value: '#006d3a' }, + { name: 'Purple', value: '#7c4dff' }, + { name: 'Orange', value: '#8b5000' }, +]; + +export const Settings: React.FC = () => { + const { theme, setTheme, seedColor, setSeedColor, reset } = useStore(); + const navigate = useNavigate(); + + const handleLogout = () => { + reset(); + navigate('/login'); + }; + + return ( +
+

Settings

+ +
+

+ + Theme +

+
+ + + +
+
+ +
+

+ + Accent Color +

+
+ {colors.map((c) => ( +
+
+ +
+ +
+
+ ); +}; diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 2ec3a38..e3a4d6b 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -5,9 +5,13 @@ interface AppState { apiDomain: string; apiKey: string; isMockMode: boolean; + theme: 'system' | 'light' | 'dark'; + seedColor: string; setApiDomain: (domain: string) => void; setApiKey: (key: string) => void; setMockMode: (isMock: boolean) => void; + setTheme: (theme: 'system' | 'light' | 'dark') => void; + setSeedColor: (color: string) => void; reset: () => void; } @@ -17,9 +21,13 @@ export const useStore = create()( apiDomain: import.meta.env.VITE_API_DOMAIN || '', apiKey: '', isMockMode: import.meta.env.VITE_MOCK_MODE === 'true', + theme: 'system', + seedColor: '#0061a4', // Default M3 Blue setApiDomain: (apiDomain) => set({ apiDomain }), setApiKey: (apiKey) => set({ apiKey }), setMockMode: (isMockMode) => set({ isMockMode }), + setTheme: (theme) => set({ theme }), + setSeedColor: (seedColor) => set({ seedColor }), reset: () => set({ apiKey: '' }), }), { diff --git a/src/styles/globals.css b/src/styles/globals.css index c803f68..20efa98 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -13,25 +13,26 @@ --md-sys-color-on-surface-variant: #43474e; --md-sys-color-outline: #73777f; --md-sys-color-error: #ba1a1a; + + --nav-width: 80px; + --bottom-nav-height: 80px; } -@media (prefers-color-scheme: dark) { - :root { - --md-sys-color-primary: #9ecaff; - --md-sys-color-on-primary: #003258; - --md-sys-color-primary-container: #00497d; - --md-sys-color-on-primary-container: #d1e4ff; - --md-sys-color-secondary: #bbc7db; - --md-sys-color-on-secondary: #253140; - --md-sys-color-secondary-container: #3b4858; - --md-sys-color-on-secondary-container: #d7e3f7; - --md-sys-color-surface: #1a1c1e; - --md-sys-color-on-surface: #e2e2e6; - --md-sys-color-surface-variant: #43474e; - --md-sys-color-on-surface-variant: #c3c7cf; - --md-sys-color-outline: #8d9199; - --md-sys-color-error: #ffb4ab; - } +:root.dark { + --md-sys-color-primary: #9ecaff; + --md-sys-color-on-primary: #003258; + --md-sys-color-primary-container: #00497d; + --md-sys-color-on-primary-container: #d1e4ff; + --md-sys-color-secondary: #bbc7db; + --md-sys-color-on-secondary: #253140; + --md-sys-color-secondary-container: #3b4858; + --md-sys-color-on-secondary-container: #d7e3f7; + --md-sys-color-surface: #1a1c1e; + --md-sys-color-on-surface: #e2e2e6; + --md-sys-color-surface-variant: #43474e; + --md-sys-color-on-surface-variant: #c3c7cf; + --md-sys-color-outline: #8d9199; + --md-sys-color-error: #ffb4ab; } * { @@ -48,14 +49,7 @@ body { transition: background-color 0.3s, color 0.3s; } -button { - cursor: pointer; - font-family: inherit; -} - -input { - font-family: inherit; -} +/* Material 3 Components */ .m3-button { display: inline-flex; @@ -69,6 +63,8 @@ input { font-size: 14px; letter-spacing: 0.1px; transition: box-shadow 0.2s, background-color 0.2s; + background-color: transparent; + color: var(--md-sys-color-primary); } .m3-button-filled { @@ -105,3 +101,97 @@ input { outline: none; border-bottom: 2px solid var(--md-sys-color-primary); } + +/* Layout and Navigation */ + +.app-container { + display: flex; + min-height: 100vh; +} + +.main-content { + flex: 1; + padding-bottom: var(--bottom-nav-height); +} + +@media (min-width: 600px) { + .main-content { + padding-bottom: 0; + margin-left: var(--nav-width); + } +} + +.navigation-rail { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--nav-width); + background-color: var(--md-sys-color-surface); + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 0; + border-right: 1px solid var(--md-sys-color-outline); + z-index: 100; +} + +.navigation-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: var(--bottom-nav-height); + background-color: var(--md-sys-color-surface-variant); + display: flex; + justify-content: space-around; + align-items: center; + padding: 0 8px; + z-index: 100; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-decoration: none; + color: var(--md-sys-color-on-surface-variant); + gap: 4px; + flex: 1; + height: 100%; + border-radius: 16px; + transition: background-color 0.2s; +} + +.nav-item-icon-wrapper { + width: 64px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + transition: background-color 0.2s; +} + +.nav-item.active .nav-item-icon-wrapper { + background-color: var(--md-sys-color-secondary-container); + color: var(--md-sys-color-on-secondary-container); +} + +.nav-item-label { + font-size: 12px; + font-weight: 500; +} + +@media (min-width: 600px) { + .navigation-bar { + display: none; + } +} + +@media (max-width: 599px) { + .navigation-rail { + display: none; + } +}