last update before handover

This commit is contained in:
2026-05-29 12:39:53 +08:00
parent 121d7f6024
commit 357e30af66
1362 changed files with 166577 additions and 387 deletions

View File

@@ -2,19 +2,19 @@ import { ChevronUp, ChevronDown } from "lucide-react";
import { Button } from "./ui/button";
export function NavigationButtons() {
const scrollAmount = 200;
const scrollAmount = 300;
const scrollUp = () => {
const panel = document.getElementById("details-panel");
if (panel) {
panel.scrollBy({ top: -scrollAmount, behavior: "smooth" });
panel.scrollBy({ top: -scrollAmount, behavior: "auto" });
}
};
const scrollDown = () => {
const panel = document.getElementById("details-panel");
if (panel) {
panel.scrollBy({ top: scrollAmount, behavior: "smooth" });
panel.scrollBy({ top: scrollAmount, behavior: "auto" });
}
};

View File

@@ -9,7 +9,9 @@ import {
HardDrive,
LogIn,
ChevronRight,
ChevronDown
ChevronDown,
HelpCircle,
HeadphonesIcon
} from "lucide-react";
interface SidebarProps {
@@ -85,6 +87,18 @@ const menuItems: MenuItem[] = [
icon: <LogIn className="w-5 h-5" />,
path: "/login",
},
{
id: "help",
label: "HELP",
icon: <HelpCircle className="w-5 h-5" />,
path: "/help",
},
{
id: "support",
label: "SUPPORT",
icon: <HeadphonesIcon className="w-5 h-5" />,
path: "/support",
},
];
export function Sidebar({ collapsed, onToggle }: SidebarProps) {

View File

@@ -43,9 +43,31 @@ export function DateTimeSettingView() {
id="timezone"
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white"
>
<option>UTC+8 (Philippine Time)</option>
<option>UTC+0 (GMT)</option>
<option>UTC+1 (CET)</option>
<option>UTC+2 (EET)</option>
<option>UTC+3 (EAT)</option>
<option>UTC+4 (GST)</option>
<option>UTC+5 (PKT)</option>
<option>UTC+5:30 (IST)</option>
<option>UTC+6 (BTT)</option>
<option>UTC+7 (ICT)</option>
<option>UTC+8 (MYT - Malaysian Time)</option>
<option>UTC+8 (PHT - Philippine Time)</option>
<option>UTC+9 (JST)</option>
<option>UTC+10 (AEST)</option>
<option>UTC+11 (AEDT)</option>
<option>UTC+12 (NZST)</option>
<option>UTC-1 (AZOT)</option>
<option>UTC-2 (BRT)</option>
<option>UTC-3 (ART)</option>
<option>UTC-4 (AST)</option>
<option>UTC-5 (EST)</option>
<option>UTC-6 (CST)</option>
<option>UTC-7 (MST)</option>
<option>UTC-8 (PST)</option>
<option>UTC-9 (AKST)</option>
<option>UTC-10 (HST)</option>
</select>
</div>
</div>

View File

@@ -1,51 +1,229 @@
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { useEffect } from 'react';
import { Card, CardContent, CardTitle } from "../ui/card";
import { BarChart3 } from "lucide-react";
import { useSensorStore } from "../../stores/sensorStore";
import {
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
ComposedChart,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
const visualizations = [
{ id: 'rainfall', title: 'Rainfall', type: 'bar' },
{ id: 'voltage', title: 'Voltage', type: 'composed' },
{ id: 'signal', title: 'Signal Strength', type: 'area' },
{ id: 'waterlevel', title: 'Water Level', type: 'line' },
{ id: 'adc', title: 'ADC Channels', type: 'multiline' },
{ id: 'temperature', title: 'Temperature', type: 'area' },
];
function generateMockData(id: string) {
const hours = Array.from({ length: 24 }, (_, i) => i);
switch (id) {
case 'rainfall':
return hours.map(h => ({ name: `${h}:00`, rainfall: Math.random() * 5 }));
case 'voltage':
return hours.map(h => ({ name: `${h}:00`, solar: 11 + Math.random() * 4, battery: 11.5 + Math.random() * 2 }));
case 'signal':
return hours.map(h => ({ name: `${h}:00`, asu: Math.floor(Math.random() * 31), dBm: -113 + Math.floor(Math.random() * 60) }));
case 'waterlevel':
return hours.map(h => ({ name: `${h}:00`, level: 1 + Math.random() * 3 }));
case 'adc':
return hours.map(h => ({ name: `${h}:00`, ch1: Math.random() * 20, ch2: Math.random() * 20, ch3: Math.random() * 20, ch4: Math.random() * 20 }));
case 'temperature':
return hours.map(h => ({ name: `${h}:00`, temp: 20 + Math.random() * 10 }));
default:
return [];
}
}
function renderChart(id: string, data: { name: string; [key: string]: number | string }[]) {
switch (id) {
case 'rainfall':
return (
<ResponsiveContainer width="100%" height={190}>
<BarChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#9CA3AF" fontSize={8} tickLine={false} />
<YAxis stroke="#9CA3AF" fontSize={8} tickLine={false} />
<Tooltip contentStyle={{ backgroundColor: '#1F2937', border: 'none', fontSize: 10 }} />
<Bar dataKey="rainfall" fill="#3B82F6" radius={[2, 2, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
case 'voltage':
return (
<ResponsiveContainer width="100%" height={190}>
<ComposedChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#9CA3AF" fontSize={8} tickLine={false} />
<YAxis stroke="#9CA3AF" fontSize={8} tickLine={false} domain={[0, 20]} />
<Tooltip contentStyle={{ backgroundColor: '#1F2937', border: 'none', fontSize: 10 }} />
<Line type="monotone" dataKey="solar" stroke="#F59E0B" dot={false} strokeWidth={2} />
<Line type="monotone" dataKey="battery" stroke="#10B981" dot={false} strokeWidth={2} />
</ComposedChart>
</ResponsiveContainer>
);
case 'signal':
return (
<ResponsiveContainer width="100%" height={190}>
<AreaChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#9CA3AF" fontSize={8} tickLine={false} />
<YAxis stroke="#9CA3AF" fontSize={8} tickLine={false} domain={[-120, 0]} />
<Tooltip contentStyle={{ backgroundColor: '#1F2937', border: 'none', fontSize: 10 }} />
<Area type="monotone" dataKey="dBm" stroke="#3B82F6" fill="#3B82F6" fillOpacity={0.3} />
</AreaChart>
</ResponsiveContainer>
);
case 'waterlevel':
return (
<ResponsiveContainer width="100%" height={190}>
<LineChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#9CA3AF" fontSize={8} tickLine={false} />
<YAxis stroke="#9CA3AF" fontSize={8} tickLine={false} domain={[0, 5]} />
<Tooltip contentStyle={{ backgroundColor: '#1F2937', border: 'none', fontSize: 10 }} />
<Line type="monotone" dataKey="level" stroke="#3B82F6" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
);
case 'adc':
return (
<ResponsiveContainer width="100%" height={190}>
<LineChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#9CA3AF" fontSize={8} tickLine={false} />
<YAxis stroke="#9CA3AF" fontSize={8} tickLine={false} domain={[0, 25]} />
<Tooltip contentStyle={{ backgroundColor: '#1F2937', border: 'none', fontSize: 10 }} />
<Line type="monotone" dataKey="ch1" stroke="#3B82F6" dot={false} strokeWidth={1} />
<Line type="monotone" dataKey="ch2" stroke="#10B981" dot={false} strokeWidth={1} />
<Line type="monotone" dataKey="ch3" stroke="#8B5CF6" dot={false} strokeWidth={1} />
<Line type="monotone" dataKey="ch4" stroke="#F59E0B" dot={false} strokeWidth={1} />
</LineChart>
</ResponsiveContainer>
);
case 'temperature':
return (
<ResponsiveContainer width="100%" height={190}>
<AreaChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#9CA3AF" fontSize={8} tickLine={false} />
<YAxis stroke="#9CA3AF" fontSize={8} tickLine={false} domain={[15, 35]} />
<Tooltip contentStyle={{ backgroundColor: '#1F2937', border: 'none', fontSize: 10 }} />
<Area type="monotone" dataKey="temp" stroke="#EF4444" fill="#EF4444" fillOpacity={0.3} />
</AreaChart>
</ResponsiveContainer>
);
default:
return null;
}
}
export function GraphView() {
const { data } = useSensorStore();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const panel = document.getElementById("details-panel");
if (!panel) return;
if (e.key === 'PageUp' || e.key === 'ArrowUp') {
e.preventDefault();
panel.scrollTop -= Math.max(300, panel.clientHeight);
} else if (e.key === 'PageDown' || e.key === 'ArrowDown') {
e.preventDefault();
panel.scrollTop += Math.max(300, panel.clientHeight);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<div className="h-full p-1 flex flex-col">
<div className="flex items-center gap-2 mb-2">
<BarChart3 className="w-5 h-5 text-blue-400" />
<h1 className="text-lg font-bold text-white">Graph View</h1>
<div className="h-full flex flex-col relative">
<div className="flex items-center justify-between p-1 bg-gray-800 border-b border-gray-700 relative z-10">
<div className="flex items-center">
<BarChart3 className="w-4 h-4 text-blue-400 mr-2" />
<h1 className="text-base font-bold text-white">Graph View</h1>
</div>
</div>
<div className="grid grid-cols-3 gap-2 mb-2">
<Card className="bg-gray-900 border-gray-700">
<CardContent className="py-3">
<div className="text-center">
<div className="text-xl font-bold text-blue-400">24h</div>
<div className="text-xs text-gray-400">Time Range</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-700">
<CardContent className="py-3">
<div className="text-center">
<div className="text-xl font-bold text-green-400">156</div>
<div className="text-xs text-gray-400">Data Points</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-700">
<CardContent className="py-3">
<div className="text-center">
<div className="text-xl font-bold text-purple-400">12.5</div>
<div className="text-xs text-gray-400">Avg Value</div>
</div>
</CardContent>
</Card>
</div>
<div className="flex-1">
{visualizations.map((viz, idx) => {
const chartData = generateMockData(viz.id);
let stats: { label: string; value: string; color: string }[] = [];
switch (viz.id) {
case 'rainfall':
stats = [
{ label: '24h', value: `${data.rainfall.today}mm`, color: 'text-blue-400' },
{ label: 'Avg', value: `${(data.rainfall.today / 24).toFixed(1)}mm`, color: 'text-purple-400' },
];
break;
case 'voltage':
stats = [
{ label: 'Solar', value: `${data.voltage.solar.toFixed(1)}V`, color: 'text-yellow-400' },
{ label: 'Battery', value: `${data.voltage.battery.toFixed(1)}V`, color: 'text-green-400' },
];
break;
case 'signal':
stats = [
{ label: 'ASU', value: `${data.communication.asu}`, color: 'text-blue-400' },
{ label: 'dBm', value: `${data.communication.dBm}`, color: 'text-green-400' },
];
break;
case 'waterlevel':
stats = [
{ label: 'Level', value: '2.5m', color: 'text-blue-400' },
{ label: 'Status', value: 'Normal', color: 'text-green-400' },
];
break;
case 'adc':
stats = [
{ label: 'CH1', value: '12.4mA', color: 'text-blue-400' },
{ label: 'CH2', value: '8.2mA', color: 'text-green-400' },
];
break;
case 'temperature':
stats = [
{ label: 'Current', value: '26.5°C', color: 'text-red-400' },
{ label: 'Max', value: '28.1°C', color: 'text-orange-400' },
];
break;
}
<Card className="bg-gray-900 border-gray-700 flex-1">
<CardHeader className="py-2">
<CardTitle className="text-white text-sm">Data Visualization</CardTitle>
</CardHeader>
<CardContent className="py-2 h-full">
<div className="h-full bg-gray-800 rounded flex items-center justify-center">
<span className="text-gray-500 text-sm">Chart visualization area</span>
</div>
</CardContent>
</Card>
return (
<div key={viz.id} data-graph-card className="mb-1">
<Card className="bg-gray-900 border-gray-700">
<CardContent className="py-1 px-2">
<div className="flex items-center gap-2 mb-1">
<CardTitle className="text-white text-sm m-0">{viz.title}</CardTitle>
{stats.map((stat, sidx) => (
<span key={sidx} className="text-xs">
<span className={stat.color}>{stat.value}</span>
<span className="text-gray-500 ml-0.5">{stat.label}</span>
</span>
))}
<span className="text-xs text-gray-600 ml-auto">{idx + 1}/{visualizations.length}</span>
</div>
{renderChart(viz.id, chartData)}
</CardContent>
</Card>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,74 @@
import { useRef, useEffect } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { components } from './MarkdownComponents';
import helpContent from '../../../../HELP.md?raw';
const tocItems = [
{ id: 'overview', label: '1. Overview' },
{ id: 'navigation', label: '2. Navigation' },
{ id: 'main-sections', label: '3. Main Sections' },
{ id: 'keyboard-shortcuts', label: '4. Keyboard Shortcuts' },
{ id: 'display-modes', label: '5. Display Modes' },
];
export function HelpView() {
const contentRef = useRef<HTMLDivElement>(null);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
const panel = document.getElementById("details-panel");
if (element && panel) {
const panelRect = panel.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const offset = elementRect.top - panelRect.top + panel.scrollTop;
panel.scrollTop = offset;
}
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const panel = document.getElementById("details-panel");
if (!panel) return;
if (e.key === 'PageUp' || e.key === 'ArrowUp') {
e.preventDefault();
panel.scrollTop -= Math.max(200, panel.clientHeight);
} else if (e.key === 'PageDown' || e.key === 'ArrowDown') {
e.preventDefault();
panel.scrollTop += Math.max(200, panel.clientHeight);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
const contentWithoutToc = helpContent.replace(/^# Table of Contents[\s\S]*?^---$/m, '');
return (
<div className="h-full overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="bg-gray-900 border border-gray-700 rounded-lg p-4 sticky top-0">
<h2 className="text-xl font-bold text-white mb-4">Table of Contents</h2>
<nav className="space-y-2">
{tocItems.map((item) => (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className="block text-blue-400 hover:underline text-left w-full"
>
{item.label}
</button>
))}
</nav>
</div>
<div id="details-panel" ref={contentRef} className="bg-gray-900 border border-gray-700 rounded-lg p-4 overflow-y-auto max-h-[calc(100vh-200px)]">
<div className="prose prose-invert max-w-none prose-sm prose-table:border-collapse prose-th:border prose-td:border prose-th:border-gray-600 prose-td:border-gray-600 prose-th:p-2 prose-td:p-2 prose-thead:bg-gray-800 prose-tr:hover:bg-gray-800">
<Markdown remarkPlugins={[remarkGfm]} components={components}>{contentWithoutToc}</Markdown>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { Components } from 'react-markdown';
export const components: Components = {
h1: ({ node, children, ...props }) => {
const text = Array.isArray(children) ? children.join('') : String(children);
const id = text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return <h1 id={id} {...props}>{children}</h1>;
},
h2: ({ node, children, ...props }) => {
const text = Array.isArray(children) ? children.join('') : String(children);
const id = text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return <h2 id={id} {...props}>{children}</h2>;
},
h3: ({ node, children, ...props }) => {
const text = Array.isArray(children) ? children.join('') : String(children);
const id = text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return <h3 id={id} {...props}>{children}</h3>;
},
};

View File

@@ -0,0 +1,55 @@
import { Mail, Phone, User } from "lucide-react";
export function SupportView() {
return (
<div className="h-full flex items-center justify-center">
<div className="bg-gray-900 border border-gray-700 rounded-lg p-8 max-w-md w-full">
<div className="text-center mb-6">
<div className="h-16 mb-4 flex items-center justify-center">
<span className="text-3xl font-bold text-blue-400">TCK</span>
<span className="text-3xl font-bold text-red-500 ml-1">e-Solutions</span>
</div>
<h1 className="text-2xl font-bold text-white">Support</h1>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-gray-800 rounded-lg">
<User className="w-5 h-5 text-blue-400" />
<div>
<p className="text-xs text-gray-500">Company</p>
<p className="text-white">TCK e-Solutions Sdn. Bhd.</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-800 rounded-lg">
<User className="w-5 h-5 text-blue-400" />
<div>
<p className="text-xs text-gray-500">Contact Person</p>
<p className="text-white">C-Fu</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-800 rounded-lg">
<Mail className="w-5 h-5 text-blue-400" />
<div>
<p className="text-xs text-gray-500">Email</p>
<a href="mailto:cloud@tck.com.my" className="text-blue-400 hover:underline">
cloud@tck.com.my
</a>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-800 rounded-lg">
<Phone className="w-5 h-5 text-blue-400" />
<div>
<p className="text-xs text-gray-500">Phone</p>
<a href="tel:+601229292929" className="text-blue-400 hover:underline">
+601229292929
</a>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -16,6 +16,8 @@ import { CalibrationView } from "./components/views/CalibrationView";
import { FlashMemoryView } from "./components/views/FlashMemoryView";
import { SettingView } from "./components/views/SettingView";
import { LoginView } from "./components/views/LoginView";
import { HelpView } from "./components/views/HelpView";
import { SupportView } from "./components/views/SupportView";
export const router = createBrowserRouter([
{
@@ -39,6 +41,8 @@ export const router = createBrowserRouter([
{ path: "flash-memory", Component: FlashMemoryView },
{ path: "setting", Component: SettingView },
{ path: "login", Component: LoginView },
{ path: "help", Component: HelpView },
{ path: "support", Component: SupportView },
],
},
]);

View File

@@ -1,4 +1,5 @@
@custom-variant dark (&:is(.dark *));
@custom-variant light (&:is(.light *));
:root {
--font-size: 16px;
@@ -188,4 +189,67 @@
#details-panel::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
}
/* Light mode overrides for hardcoded dark backgrounds */
.light {
--bg-gray-900: #f9fafb;
--bg-gray-800: #f3f4f6;
--bg-gray-700: #e5e7eb;
--bg-gray-950: #ffffff;
}
.light .bg-gray-900 {
background-color: #ffffff !important;
border-color: #e5e7eb !important;
}
.light .bg-gray-900 \* {
color: #111827 !important;
}
.light .bg-gray-800 {
background-color: #f3f4f6 !important;
border-color: #d1d5db !important;
}
.light .bg-gray-800 \* {
color: #111827 !important;
}
.light .bg-gray-700 {
background-color: #e5e7eb !important;
}
.light .bg-gray-700 \* {
color: #111827 !important;
}
.light .text-white {
color: #111827 !important;
}
.light .text-gray-300,
.light .text-gray-400,
.light .text-gray-500,
.light .text-gray-600 {
color: #4b5563 !important;
}
.light .border-gray-700,
.light .border-gray-600,
.light .border-gray-500 {
border-color: #d1d5db !important;
}
.light .bg-gray-950 {
background-color: #f9fafb !important;
}
.light .bg-gray-950 \* {
color: #111827 !important;
}
.light .hover\:bg-gray-800:hover {
background-color: #e5e7eb !important;
}