Skip to content

Commit 4cec348

Browse files
authored
Merge pull request #36 from Western-Formula-Racing/settings
Add Settings modal and integrate into UI
2 parents e3d10c6 + 358c673 commit 4cec348

4 files changed

Lines changed: 175 additions & 15 deletions

File tree

pecan/src/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
22
import "./App.css";
33
import Sidebar from "./components/Sidebar";
44
import Hamburger from "./components/HamburgerMenu";
5+
import SettingsModal from "./components/SettingsModal";
56
import {
67
loadDBCFromCache,
78
usingCachedDBC,
@@ -12,6 +13,7 @@ import { DefaultBanner, CacheBanner } from "./components/Banners";
1213

1314
function App() {
1415
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
16+
const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false);
1517

1618
const [displayCacheBanner, setDisplayCacheBanner] = useState<boolean>(false);
1719
const [displayDefaultBanner, setDisplayDefaultBanner] =
@@ -26,6 +28,9 @@ function App() {
2628
toggleCache: () => setDisplayCacheBanner((o) => !o),
2729
};
2830

31+
const openSettings = () => setIsSettingsOpen(true);
32+
const closeSettings = () => setIsSettingsOpen(false);
33+
2934
useEffect(() => {
3035
(async () => {
3136
console.log("[App] Loading DBC from cache...");
@@ -63,20 +68,22 @@ function App() {
6368
<div className="h-screen flex flex-row overflow-hidden">
6469
<div className={`h-screen transition-all duration-300 ease-in-out flex-shrink-0 ${isSidebarOpen ? 'lg:w-2/9 md:w-2/5 sm:w-3/5 w-full' : 'w-[60px]'}`}>
6570
{!isSidebarOpen && <Hamburger trigger={() => setIsSidebarOpen(true)} />}
66-
{isSidebarOpen && <Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />}
71+
{isSidebarOpen && <Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} onOpenSettings={openSettings} />}
6772
</div>
6873

6974
{/* Main content area, Outlet element is needed to display the rendered child pages received from the routes */}
7075
<main id="main-content" className="flex-1 h-full min-w-0">
7176
<DefaultBanner
7277
open={displayDefaultBanner}
7378
onClose={() => setDisplayDefaultBanner(false)}
79+
onOpenSettings={openSettings}
7480
/>
7581
<CacheBanner
7682
open={displayCacheBanner}
7783
onClose={() => setDisplayCacheBanner(false)}
7884
/>
79-
<Outlet context={{ isSidebarOpen, ...bannerApi }} />
85+
<Outlet context={{ isSidebarOpen, openSettings, ...bannerApi }} />
86+
<SettingsModal isOpen={isSettingsOpen} onClose={closeSettings} bannerApi={bannerApi} />
8087
</main>
8188

8289

pecan/src/components/Banners.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { DefaultBanner, CacheBanner };
55
interface InputProps {
66
open: boolean;
77
onClose: () => void;
8+
onOpenSettings?: () => void;
89
}
910

1011
const handleRevert = async () => {
@@ -16,7 +17,7 @@ const handleRevert = async () => {
1617
} catch (error) {
1718
console.warn("[handleRevert] Cache API not available:", error instanceof Error ? error.message : String(error));
1819
}
19-
20+
2021
// Clear localStorage
2122
try {
2223
localStorage.removeItem('dbc-file-content');
@@ -25,13 +26,21 @@ const handleRevert = async () => {
2526
} catch (error) {
2627
console.error("[handleRevert] Error clearing localStorage:", error);
2728
}
28-
29+
2930
forceCache(false);
3031
globalThis.location.reload();
3132
};
3233

33-
function DefaultBanner({ open, onClose }: Readonly<InputProps>) {
34+
function DefaultBanner({ open, onClose, onOpenSettings }: Readonly<InputProps>) {
3435
if (!open) return null;
36+
37+
const handleOpenSettings = () => {
38+
onClose();
39+
if (onOpenSettings) {
40+
onOpenSettings();
41+
}
42+
};
43+
3544
return (
3645
<div className="fixed bottom-0 left-0 right-0 z-50 flex flex-row w-full bg-dropdown-menu-bg justify-between items-center box-border px-4 py-3 shadow-lg border-t border-gray-600">
3746
<div className="w-[20%]"></div>
@@ -43,7 +52,7 @@ function DefaultBanner({ open, onClose }: Readonly<InputProps>) {
4352
</div>
4453
<div className="flex flex-row w-[20%] items-center justify-end gap-4 pe-4">
4554
<button
46-
onClick={() => (globalThis.location.href = "/settings")}
55+
onClick={handleOpenSettings}
4756
className="bg-banner-button hover:bg-banner-button-hover px-6 py-2 cursor-pointer text-center text-[14pt] font-semibold text-white rounded-md transition-colors shadow-sm"
4857
style={{ borderRadius: '0.375rem' }}
4958
>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { forceCache } from "../utils/canProcessor";
2+
3+
interface SettingsModalProps {
4+
isOpen: boolean;
5+
onClose: () => void;
6+
bannerApi: {
7+
showDefault: () => void;
8+
showCache: () => void;
9+
hideDefault: () => void;
10+
hideCache: () => void;
11+
toggleDefault: () => void;
12+
toggleCache: () => void;
13+
};
14+
}
15+
16+
async function uploadFileToCache(file: File) {
17+
if (!file) return;
18+
19+
const fileContent = await file.text();
20+
21+
// Try Cache API first (requires secure context)
22+
try {
23+
const cache = await caches.open("dbc-files");
24+
const cacheKey = "cache.dbc";
25+
26+
console.log("[uploadFileToCache] Uploading DBC file to cache...");
27+
await cache.delete(cacheKey);
28+
const request = new Request(cacheKey);
29+
const res = new Response(fileContent, {
30+
headers: { "Content-Type": "text/plain; charset=utf-8" },
31+
});
32+
await cache.put(request, res);
33+
console.log("[uploadFileToCache] Successfully cached DBC file");
34+
35+
// Verify it was cached
36+
const verify = await cache.match(cacheKey);
37+
console.log(
38+
"[uploadFileToCache] Verification - cached file exists:",
39+
!!verify,
40+
);
41+
} catch (error) {
42+
console.warn(
43+
"[uploadFileToCache] Cache API not available, using localStorage fallback:",
44+
error instanceof Error ? error.message : String(error),
45+
);
46+
}
47+
48+
// Always save to localStorage as fallback (works in non-secure contexts)
49+
try {
50+
localStorage.setItem("dbc-file-content", fileContent);
51+
console.log("[uploadFileToCache] Successfully saved DBC to localStorage");
52+
} catch (error) {
53+
console.error("[uploadFileToCache] Error saving to localStorage:", error);
54+
}
55+
}
56+
57+
function SettingsModal({ isOpen, onClose, bannerApi }: Readonly<SettingsModalProps>) {
58+
if (!isOpen) return null;
59+
60+
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
61+
const file = e.target.files?.[0];
62+
if (!file) return;
63+
await uploadFileToCache(file);
64+
65+
// Set localStorage flag to indicate cache is active
66+
localStorage.setItem("dbc-cache-active", "true");
67+
68+
bannerApi.showCache();
69+
bannerApi.hideDefault();
70+
forceCache(true);
71+
onClose();
72+
globalThis.location.reload();
73+
};
74+
75+
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
76+
if (e.target === e.currentTarget) {
77+
onClose();
78+
}
79+
};
80+
81+
return (
82+
<div
83+
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
84+
onClick={handleBackdropClick}
85+
>
86+
<div className="relative bg-sidebar rounded-xl shadow-2xl border border-gray-600 w-[66%] h-[80%] p-6 flex flex-col animate-in fade-in zoom-in-95 duration-200">
87+
{/* Close button */}
88+
<button
89+
onClick={onClose}
90+
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors cursor-pointer"
91+
aria-label="Close settings"
92+
>
93+
<svg
94+
xmlns="http://www.w3.org/2000/svg"
95+
className="h-6 w-6"
96+
fill="none"
97+
viewBox="0 0 24 24"
98+
stroke="currentColor"
99+
>
100+
<path
101+
strokeLinecap="round"
102+
strokeLinejoin="round"
103+
strokeWidth={2}
104+
d="M6 18L18 6M6 6l12 12"
105+
/>
106+
</svg>
107+
</button>
108+
109+
{/* Header */}
110+
<h2 className="text-2xl font-semibold text-white mb-6">Settings</h2>
111+
112+
{/* Settings content area - scrollable for future settings */}
113+
<div className="flex-1 overflow-y-auto space-y-4">
114+
{/* DBC Upload Section - compact single row */}
115+
<div className="flex flex-row w-full rounded-lg text-white bg-option justify-between items-center px-4 py-3">
116+
<span className="text-sm font-medium">Custom DBC File</span>
117+
<label
118+
htmlFor="dbc-upload-modal"
119+
className="bg-banner-button hover:bg-banner-button-hover px-4 py-1.5 cursor-pointer text-center text-sm font-semibold text-white rounded-md transition-colors shadow-sm"
120+
>
121+
Upload DBC
122+
</label>
123+
<input
124+
className="sr-only"
125+
id="dbc-upload-modal"
126+
type="file"
127+
accept=".dbc"
128+
onChange={handleChange}
129+
/>
130+
</div>
131+
132+
{/* Future settings will go here */}
133+
</div>
134+
</div>
135+
</div>
136+
);
137+
}
138+
139+
export default SettingsModal;

pecan/src/components/Sidebar.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import { NavLink } from "react-router";
77
interface InputProps {
88
isOpen: boolean;
99
onClose: () => void;
10+
onOpenSettings: () => void;
1011
}
1112

12-
function Sidebar({ onClose, isOpen }: Readonly<InputProps>) {
13+
function Sidebar({ onClose, isOpen, onOpenSettings }: Readonly<InputProps>) {
14+
const handleSettingsClick = () => {
15+
onClose();
16+
onOpenSettings();
17+
};
18+
1319
return (
1420
<div>
1521
{/* Listener for outside of sidebar clicks */}
@@ -22,8 +28,8 @@ function Sidebar({ onClose, isOpen }: Readonly<InputProps>) {
2228

2329
<div
2430
className={`fixed top-0 left-0 h-full lg:w-2/9 md:w-2/5 sm:w-3/5 w-full flex flex-col z-50 transform transition-all duration-450 overflow-y-auto overscroll-contain ${isOpen
25-
? "translate-x-0 opacity-100"
26-
: "-translate-x-full opacity-0 pointer-events-none"
31+
? "translate-x-0 opacity-100"
32+
: "-translate-x-full opacity-0 pointer-events-none"
2733
}`}
2834
>
2935
<div className="bg-sidebar z-100 w-[98%] h-full flex flex-col justify-between">
@@ -74,15 +80,14 @@ function Sidebar({ onClose, isOpen }: Readonly<InputProps>) {
7480
<img src={avatar} alt="avatar" width={30} height={30} />
7581
<span className="text-sidebarfg">Account</span>
7682
</NavLink>
77-
{/* Should go to /settings*/}
78-
<NavLink
79-
to={"/settings"}
80-
className="!no-underline flex flex-row space-x-6 text-md ml-4"
81-
onClick={onClose}
83+
{/* Settings - opens modal */}
84+
<button
85+
onClick={handleSettingsClick}
86+
className="!no-underline flex flex-row space-x-6 text-md ml-4 cursor-pointer bg-transparent border-none text-left"
8287
>
8388
<img src={settings} alt="settings" width={30} height={30} />
8489
<span className="text-sidebarfg">Settings and Preferences</span>
85-
</NavLink>
90+
</button>
8691
</footer>
8792
</div>
8893
</div>

0 commit comments

Comments
 (0)