Add dark mode theme support across the UI
Introduce light/dark/system theme toggle with Bootstrap data-bs-theme and theme-aware surfaces and components. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -224,7 +224,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Call log items */
|
/* Call log items */
|
||||||
.bg-light.text-dark.border {
|
.bg-light.text-dark.border,
|
||||||
|
.bg-body-secondary.text-body-emphasis.border {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
border: 1px solid #666 !important;
|
border: 1px solid #666 !important;
|
||||||
color: black !important;
|
color: black !important;
|
||||||
|
|||||||
+12
-10
@@ -13,6 +13,7 @@ import Search from './components/Search'
|
|||||||
import Summary from './components/Summary'
|
import Summary from './components/Summary'
|
||||||
import ChangePasswordModal from './components/ChangePasswordModal'
|
import ChangePasswordModal from './components/ChangePasswordModal'
|
||||||
import SettingsModal from './components/SettingsModal'
|
import SettingsModal from './components/SettingsModal'
|
||||||
|
import ThemeToggle from './components/ThemeToggle'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
|
||||||
@@ -190,7 +191,7 @@ function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vh-100 d-flex flex-column bg-light">
|
<div className="vh-100 d-flex flex-column bg-body-tertiary">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-primary bg-gradient text-white py-1 px-2 shadow" style={{zIndex: 1030}}>
|
<header className="bg-primary bg-gradient text-white py-1 px-2 shadow" style={{zIndex: 1030}}>
|
||||||
<div className="d-flex justify-content-between align-items-center">
|
<div className="d-flex justify-content-between align-items-center">
|
||||||
@@ -201,6 +202,7 @@ function App() {
|
|||||||
<h1 className="h5 mb-0 fw-bold">SMS Backup Viewer</h1>
|
<h1 className="h5 mb-0 fw-bold">SMS Backup Viewer</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowUpload(true)}
|
onClick={() => setShowUpload(true)}
|
||||||
className="btn btn-light btn-sm shadow-sm d-flex align-items-center gap-1"
|
className="btn btn-light btn-sm shadow-sm d-flex align-items-center gap-1"
|
||||||
@@ -256,7 +258,7 @@ function App() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* View Switcher */}
|
{/* View Switcher */}
|
||||||
<div className="bg-white border-bottom shadow-sm">
|
<div className="bg-body-tertiary border-bottom shadow-sm">
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<ul className="nav nav-tabs border-0">
|
<ul className="nav nav-tabs border-0">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
@@ -319,7 +321,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date Filter */}
|
{/* Date Filter */}
|
||||||
<div className="date-filter-container bg-white border-bottom shadow-sm" style={{zIndex: 1025, position: 'relative'}}>
|
<div className="date-filter-container bg-body-tertiary border-bottom shadow-sm" style={{zIndex: 1025, position: 'relative'}}>
|
||||||
<DateFilter
|
<DateFilter
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
@@ -336,9 +338,9 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
{/* Conversation List */}
|
{/* Conversation List */}
|
||||||
<div
|
<div
|
||||||
className={`conversation-sidebar bg-white rounded-3 shadow overflow-hidden border ${showSidebar ? 'show' : ''}`}
|
className={`conversation-sidebar bg-body-tertiary rounded-3 shadow overflow-hidden border ${showSidebar ? 'show' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="bg-light border-bottom p-1">
|
<div className="bg-body-tertiary border-bottom p-1">
|
||||||
<h2 className="h6 mb-1 d-flex align-items-center gap-1 px-1">
|
<h2 className="h6 mb-1 d-flex align-items-center gap-1 px-1">
|
||||||
<svg style={{width: '1rem', height: '1rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1rem', height: '1rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||||
@@ -369,7 +371,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message Thread */}
|
{/* Message Thread */}
|
||||||
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border message-thread-container" style={{minWidth: 0}}>
|
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border message-thread-container" style={{minWidth: 0}}>
|
||||||
<MessageThread
|
<MessageThread
|
||||||
conversation={selectedConversation}
|
conversation={selectedConversation}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
@@ -380,7 +382,7 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
) : activeView === 'search' ? (
|
) : activeView === 'search' ? (
|
||||||
/* Search View */
|
/* Search View */
|
||||||
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
||||||
<Search
|
<Search
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
@@ -396,7 +398,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : activeView === 'calls' ? (
|
) : activeView === 'calls' ? (
|
||||||
/* Calls View */
|
/* Calls View */
|
||||||
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
||||||
<Calls
|
<Calls
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
@@ -404,7 +406,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : activeView === 'summary' ? (
|
) : activeView === 'summary' ? (
|
||||||
/* Summary View */
|
/* Summary View */
|
||||||
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
||||||
<Summary
|
<Summary
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
@@ -412,7 +414,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Activity View */
|
/* Activity View */
|
||||||
<div className="flex-fill bg-white rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
<div className="flex-fill bg-body-secondary rounded-3 shadow overflow-hidden border" style={{minWidth: 0}}>
|
||||||
<Activity
|
<Activity
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ function Activity({ startDate, endDate }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 d-flex flex-column">
|
<div className="h-100 d-flex flex-column">
|
||||||
<div className="bg-light border-bottom p-3">
|
<div className="bg-body-tertiary border-bottom p-3">
|
||||||
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
|
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
|
||||||
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ function Calls({ startDate, endDate }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 d-flex flex-column">
|
<div className="h-100 d-flex flex-column">
|
||||||
<div className="bg-light border-bottom p-3">
|
<div className="bg-body-tertiary border-bottom p-3">
|
||||||
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
|
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
|
||||||
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function ConversationList({ conversations, selectedConversation, onSelectConvers
|
|||||||
style={{cursor: 'pointer'}}
|
style={{cursor: 'pointer'}}
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-start gap-2">
|
<div className="d-flex align-items-start gap-2">
|
||||||
<div className="flex-shrink-0 mt-1 p-2 rounded-circle bg-white shadow-sm">
|
<div className="flex-shrink-0 mt-1 p-2 rounded-circle bg-body-tertiary shadow-sm">
|
||||||
{getConversationIcon(conv.type)}
|
{getConversationIcon(conv.type)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-fill min-w-0" style={{overflow: 'hidden'}}>
|
<div className="flex-fill min-w-0" style={{overflow: 'hidden'}}>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function DateFilter({ startDate, endDate, minDate, maxDate, onStartDateChange, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-1 bg-light">
|
<div className="px-2 py-1 bg-body-tertiary">
|
||||||
<div className="d-flex align-items-center gap-1 gap-sm-2 small">
|
<div className="d-flex align-items-center gap-1 gap-sm-2 small">
|
||||||
<svg style={{width: '1rem', height: '1rem'}} className="text-primary flex-shrink-0 d-none d-sm-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1rem', height: '1rem'}} className="text-primary flex-shrink-0 d-none d-sm-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
{/* Placeholder shown before loading or while loading */}
|
{/* Placeholder shown before loading or while loading */}
|
||||||
{!src && !vcfData && !error && (
|
{!src && !vcfData && !error && (
|
||||||
<div
|
<div
|
||||||
className="bg-light rounded d-flex align-items-center justify-content-center position-relative overflow-hidden"
|
className="bg-body-tertiary rounded d-flex align-items-center justify-content-center position-relative overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
aspectRatio: isVideo ? '16/9' : isAudio ? 'auto' : '3/4', // Common phone camera ratio
|
aspectRatio: isVideo ? '16/9' : isAudio ? 'auto' : '3/4', // Common phone camera ratio
|
||||||
@@ -307,7 +307,7 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
<VCardPreview vcfText={vcfData} messageId={messageId} />
|
<VCardPreview vcfText={vcfData} messageId={messageId} />
|
||||||
)}
|
)}
|
||||||
{!isImage && !isVideo && !isAudio && !isVCard && (
|
{!isImage && !isVideo && !isAudio && !isVCard && (
|
||||||
<div className="small p-2 rounded bg-light d-flex align-items-center gap-1">
|
<div className="small p-2 rounded bg-body-tertiary d-flex align-items-center gap-1">
|
||||||
<svg style={{width: '1rem', height: '1rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1rem', height: '1rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import ThemeToggle from './ThemeToggle'
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const [isLogin, setIsLogin] = useState(true)
|
const [isLogin, setIsLogin] = useState(true)
|
||||||
@@ -64,7 +65,10 @@ function Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-vh-100 d-flex align-items-center justify-content-center bg-light">
|
<div className="min-vh-100 d-flex align-items-center justify-content-center bg-body-tertiary position-relative">
|
||||||
|
<div className="position-absolute top-0 end-0 p-3" style={{ zIndex: 10 }}>
|
||||||
|
<ThemeToggle variant="surface" />
|
||||||
|
</div>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
<div className="col-md-6 col-lg-4">
|
<div className="col-md-6 col-lg-4">
|
||||||
|
|||||||
@@ -22,6 +22,11 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode: bring the placeholder tile in line with Bootstrap's body-tertiary value. */
|
||||||
|
html[data-bs-theme="dark"] .media-grid-item {
|
||||||
|
background-color: #2c3034;
|
||||||
|
}
|
||||||
|
|
||||||
.media-thumbnail {
|
.media-thumbnail {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
|
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
|
||||||
|
import { useTheme } from '../contexts/ThemeContext'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
@@ -8,6 +9,8 @@ import MediaGrid from './MediaGrid'
|
|||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
|
||||||
|
|
||||||
function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
||||||
|
const { resolvedTheme } = useTheme()
|
||||||
|
const isDark = resolvedTheme === 'dark'
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [items, setItems] = useState([])
|
const [items, setItems] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -509,12 +512,12 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
|||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return (
|
return (
|
||||||
<div className="d-flex align-items-center justify-content-center h-100 text-muted">
|
<div className="d-flex align-items-center justify-content-center h-100 text-body-secondary">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<svg style={{width: '5rem', height: '5rem'}} className="mx-auto mb-3 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '5rem', height: '5rem'}} className="mx-auto mb-3 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="h5 text-dark">Select a conversation</p>
|
<p className="h5 text-body-emphasis mb-2">Select a conversation</p>
|
||||||
<p className="small mt-2">Choose a conversation from the list to view messages</p>
|
<p className="small mt-2">Choose a conversation from the list to view messages</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -558,7 +561,7 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Thread Header */}
|
{/* Thread Header */}
|
||||||
<div className="bg-light border-bottom p-2 p-md-4 shadow-sm">
|
<div className="bg-body-tertiary border-bottom p-2 p-md-4 shadow-sm">
|
||||||
<div className="d-flex align-items-center gap-2 gap-md-3">
|
<div className="d-flex align-items-center gap-2 gap-md-3">
|
||||||
<div className="p-2 p-md-3 rounded-circle bg-primary bg-gradient shadow">
|
<div className="p-2 p-md-3 rounded-circle bg-primary bg-gradient shadow">
|
||||||
{isCallLog ? (
|
{isCallLog ? (
|
||||||
@@ -632,7 +635,7 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div ref={scrollContainerRef} className="flex-fill overflow-auto p-2 p-md-4 bg-light">
|
<div ref={scrollContainerRef} className="flex-fill overflow-auto p-2 p-md-4 bg-body-secondary">
|
||||||
{showMediaOnly && !isCallLog ? (
|
{showMediaOnly && !isCallLog ? (
|
||||||
// Media Grid View
|
// Media Grid View
|
||||||
<MediaGrid conversation={conversation} startDate={startDate} endDate={endDate} />
|
<MediaGrid conversation={conversation} startDate={startDate} endDate={endDate} />
|
||||||
@@ -710,7 +713,7 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
|||||||
const typeInfo = getCallTypeInfo(call.type)
|
const typeInfo = getCallTypeInfo(call.type)
|
||||||
return (
|
return (
|
||||||
<div key={`call-${call.id}`} className="d-flex justify-content-center my-1">
|
<div key={`call-${call.id}`} className="d-flex justify-content-center my-1">
|
||||||
<div className="badge bg-light text-dark border px-3 py-2 d-flex align-items-center gap-2" style={{fontSize: '0.75rem'}}>
|
<div className="badge bg-body-secondary text-body-emphasis border px-3 py-2 d-flex align-items-center gap-2" style={{fontSize: '0.75rem'}}>
|
||||||
<span className={typeInfo.color} style={{fontSize: '1rem'}}>{typeInfo.icon}</span>
|
<span className={typeInfo.color} style={{fontSize: '1rem'}}>{typeInfo.icon}</span>
|
||||||
<span className={`fw-semibold ${typeInfo.color}`}>{typeInfo.label} call</span>
|
<span className={`fw-semibold ${typeInfo.color}`}>{typeInfo.label} call</span>
|
||||||
<span className="text-muted">·</span>
|
<span className="text-muted">·</span>
|
||||||
@@ -750,6 +753,8 @@ function MessageThread({ conversation, startDate, endDate, messageLimit }) {
|
|||||||
className={`card shadow-sm ${
|
className={`card shadow-sm ${
|
||||||
isSent
|
isSent
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
|
: isDark
|
||||||
|
? 'bg-body-tertiary text-body'
|
||||||
: 'bg-white'
|
: 'bg-white'
|
||||||
} ${
|
} ${
|
||||||
isHighlighted
|
isHighlighted
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function Search({ searchQuery, setSearchQuery, results, setResults, loading, set
|
|||||||
return (
|
return (
|
||||||
<div className="h-100 d-flex flex-column">
|
<div className="h-100 d-flex flex-column">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-light border-bottom p-3">
|
<div className="bg-body-tertiary border-bottom p-3">
|
||||||
<h2 className="h5 mb-3 d-flex align-items-center gap-2">
|
<h2 className="h5 mb-3 d-flex align-items-center gap-2">
|
||||||
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Summary charts — dark-mode surface + readable axis/tooltip text.
|
||||||
|
*
|
||||||
|
* Selectors are intentionally scoped to the Summary page only, using the
|
||||||
|
* existing `.card` + `.border-*` stat cards and the plain `.card` chart
|
||||||
|
* cards that are already in the DOM from Summary.jsx. No JSX changes are
|
||||||
|
* required; this file is imported only by Summary.jsx.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Stats cards / chart cards: re-colorize the card body in dark mode. */
|
||||||
|
html[data-bs-theme="dark"] .summary-view .card {
|
||||||
|
background-color: var(--bs-body-tertiary);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .summary-view .card-body,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .border-primary .card-body,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .border-success .card-body,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .border-info .card-body,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .border-warning .card-body {
|
||||||
|
background-color: var(--bs-body-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recharts SVG canvas */
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-wrapper {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Axis lines and ticks */
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-cartesian-axis-line,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-polar-axis-angle-axis line,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-polar-axis-radial-axis line {
|
||||||
|
stroke: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-cartesian-axis-tick-value,
|
||||||
|
html[data-bs-theme="dark"]
|
||||||
|
.summary-view
|
||||||
|
.recharts-polar-axis-angle-axis-tick-value,
|
||||||
|
html[data-bs-theme="dark"]
|
||||||
|
.summary-view
|
||||||
|
.recharts-polar-axis-radial-axis-tick-value {
|
||||||
|
fill: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid */
|
||||||
|
html[data-bs-theme="dark"]
|
||||||
|
.summary-view
|
||||||
|
.recharts-cartesian-grid-horizontal
|
||||||
|
line,
|
||||||
|
html[data-bs-theme="dark"]
|
||||||
|
.summary-view
|
||||||
|
.recharts-cartesian-grid-vertical
|
||||||
|
line {
|
||||||
|
stroke: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip popover */
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-tooltip-wrapper {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-default-tooltip {
|
||||||
|
background-color: var(--bs-body-tertiary) !important;
|
||||||
|
border-color: var(--bs-border-color) !important;
|
||||||
|
color: var(--bs-body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legend */
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-legend-item-text {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label text */
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-pie-label-text,
|
||||||
|
html[data-bs-theme="dark"] .summary-view .recharts-bar-label-text {
|
||||||
|
fill: var(--bs-body-color);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||||
PieChart, Pie, Cell, LineChart, Line, Legend
|
PieChart, Pie, Cell, LineChart, Line, Legend
|
||||||
} from 'recharts'
|
} from 'recharts'
|
||||||
|
import './Summary.css'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api'
|
||||||
|
|
||||||
@@ -114,8 +115,8 @@ function Summary({ startDate, endDate }) {
|
|||||||
const truncate = (str, max) => str.length > max ? str.slice(0, max) + '…' : str
|
const truncate = (str, max) => str.length > max ? str.slice(0, max) + '…' : str
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 d-flex flex-column">
|
<div className="h-100 d-flex flex-column summary-view">
|
||||||
<div className="bg-light border-bottom p-3">
|
<div className="bg-body-tertiary border-bottom p-3">
|
||||||
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
|
<h2 className="h5 mb-0 d-flex align-items-center gap-2">
|
||||||
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1.25rem', height: '1.25rem'}} className="text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useTheme } from '../contexts/ThemeContext'
|
||||||
|
|
||||||
|
const THEME_LABELS = {
|
||||||
|
light: 'Light mode',
|
||||||
|
dark: 'Dark mode',
|
||||||
|
system: 'System theme',
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEXT_THEME_LABELS = {
|
||||||
|
light: 'dark mode',
|
||||||
|
dark: 'system theme',
|
||||||
|
system: 'light mode',
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemeToggle({ variant = 'header' }) {
|
||||||
|
const { theme, resolvedTheme, toggle } = useTheme()
|
||||||
|
const variantClass =
|
||||||
|
variant === 'surface' ? 'theme-toggle-btn-surface' : 'theme-toggle-btn'
|
||||||
|
|
||||||
|
const title = `${THEME_LABELS[theme]}${theme === 'system' ? ` (${resolvedTheme})` : ''}. Switch to ${NEXT_THEME_LABELS[theme]}.`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
className={`btn btn-sm ${variantClass} p-1 d-inline-flex align-items-center justify-content-center`}
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? (
|
||||||
|
<svg
|
||||||
|
width="1.1rem"
|
||||||
|
height="1.1rem"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
|
||||||
|
</svg>
|
||||||
|
) : theme === 'light' ? (
|
||||||
|
<svg
|
||||||
|
width="1.1rem"
|
||||||
|
height="1.1rem"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="1.1rem"
|
||||||
|
height="1.1rem"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||||
|
<path d="M8 21h8M12 17v4" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeToggle
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'sbv-theme'
|
||||||
|
const THEME_ORDER = ['light', 'dark', 'system']
|
||||||
|
|
||||||
|
const ThemeContext = createContext(null)
|
||||||
|
|
||||||
|
function getSystemTheme() {
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light'
|
||||||
|
}
|
||||||
|
return 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTheme(preference) {
|
||||||
|
if (preference === 'system') {
|
||||||
|
return getSystemTheme()
|
||||||
|
}
|
||||||
|
return preference
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredPreference() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (saved === 'dark' || saved === 'light' || saved === 'system') {
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable (e.g. private mode on some browsers)
|
||||||
|
}
|
||||||
|
return 'system'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState(readStoredPreference)
|
||||||
|
const [resolvedTheme, setResolvedTheme] = useState(() =>
|
||||||
|
resolveTheme(readStoredPreference()),
|
||||||
|
)
|
||||||
|
|
||||||
|
const applyResolvedTheme = useCallback((resolved) => {
|
||||||
|
setResolvedTheme(resolved)
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', resolved)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyResolvedTheme(resolveTheme(theme))
|
||||||
|
}, [theme, applyResolvedTheme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme !== 'system' || !window.matchMedia) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
const onChange = () => applyResolvedTheme(getSystemTheme())
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', onChange)
|
||||||
|
return () => mediaQuery.removeEventListener('change', onChange)
|
||||||
|
}, [theme, applyResolvedTheme])
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setTheme((current) => {
|
||||||
|
const index = THEME_ORDER.indexOf(current)
|
||||||
|
const next = THEME_ORDER[(index + 1) % THEME_ORDER.length]
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, next)
|
||||||
|
} catch {
|
||||||
|
// ignore storage errors
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, resolvedTheme, toggle }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -1,8 +1,121 @@
|
|||||||
|
/* Theme toggle in primary header: high contrast on blue gradient */
|
||||||
|
.theme-toggle-btn {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.22);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-btn:hover,
|
||||||
|
.theme-toggle-btn:focus-visible {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
|
border-color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme toggle on body/card surfaces (e.g. login page) */
|
||||||
|
.theme-toggle-btn-surface {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: var(--bs-box-shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-btn-surface:hover,
|
||||||
|
.theme-toggle-btn-surface:focus-visible {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
background-color: var(--bs-secondary-bg);
|
||||||
|
border-color: var(--bs-border-color-translucent, var(--bs-border-color));
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode: ensure body inherits Bootstrap's dark custom properties. */
|
||||||
|
html[data-bs-theme="dark"] body {
|
||||||
|
background-color: #212529;
|
||||||
|
color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode: legacy utilities that force light-theme colors */
|
||||||
|
html[data-bs-theme="dark"] .text-dark {
|
||||||
|
color: var(--bs-body-emphasis-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .bg-light.text-dark,
|
||||||
|
html[data-bs-theme="dark"] .badge.bg-light {
|
||||||
|
background-color: var(--bs-secondary-bg) !important;
|
||||||
|
color: var(--bs-body-color) !important;
|
||||||
|
border-color: var(--bs-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode: slightly brighter secondary copy on tertiary surfaces */
|
||||||
|
html[data-bs-theme="dark"] .text-muted,
|
||||||
|
html[data-bs-theme="dark"] .text-body-secondary {
|
||||||
|
color: #adb5bd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .text-body-emphasis {
|
||||||
|
color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode: react-datepicker popup and inputs */
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker {
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border-color: var(--bs-border-color);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__header {
|
||||||
|
background-color: var(--bs-secondary-bg);
|
||||||
|
border-bottom-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__current-month,
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__day-name,
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__day,
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__time-name {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__day:hover,
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__day--keyboard-selected {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__day--outside-month {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] .react-datepicker__navigation-icon::before {
|
||||||
|
border-color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode: subdued scrollbar for WebKit/Blink */
|
||||||
|
html[data-bs-theme="dark"] ::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] ::-webkit-scrollbar-track {
|
||||||
|
background: #1a1d20;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] ::-webkit-scrollbar-thumb {
|
||||||
|
background: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import axios from 'axios'
|
|||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { AuthProvider } from './contexts/AuthContext'
|
import { AuthProvider } from './contexts/AuthContext'
|
||||||
|
import { ThemeProvider } from './contexts/ThemeContext'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import Login from './components/Login.jsx'
|
import Login from './components/Login.jsx'
|
||||||
import PrintView from './components/PrintView.jsx'
|
import PrintView from './components/PrintView.jsx'
|
||||||
@@ -16,6 +17,7 @@ axios.defaults.withCredentials = true
|
|||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
@@ -31,6 +33,7 @@ createRoot(document.getElementById('root')).render(
|
|||||||
} />
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user