mirror of
https://github.com/dpup/meshstream.git
synced 2026-08-09 10:12:52 +02:00
Working on UI
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { WifiOff, Wifi, WifiLow, Activity, AlertCircle } from "lucide-react";
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
|
||||
status,
|
||||
}) => {
|
||||
// Determine connection status type
|
||||
const getStatusInfo = () => {
|
||||
if (status.includes("error") || status.includes("Error")) {
|
||||
return {
|
||||
icon: <AlertCircle className="h-5 w-5 text-red-400" />,
|
||||
text: "Connection Error",
|
||||
colorClass: "text-red-400",
|
||||
};
|
||||
} else if (status.includes("Reconnecting")) {
|
||||
return {
|
||||
icon: <WifiLow className="h-5 w-5 text-amber-400" />,
|
||||
text: "Reconnecting",
|
||||
colorClass: "text-amber-400",
|
||||
};
|
||||
} else if (status.includes("Connecting")) {
|
||||
return {
|
||||
icon: <Activity className="h-5 w-5 text-blue-400" />,
|
||||
text: "Connecting",
|
||||
colorClass: "text-blue-400",
|
||||
};
|
||||
} else if (status.includes("Connected")) {
|
||||
return {
|
||||
icon: <Wifi className="h-5 w-5 text-green-400" />,
|
||||
text: "Connected",
|
||||
colorClass: "text-green-400",
|
||||
};
|
||||
} else {
|
||||
// Default state or unknown status
|
||||
return {
|
||||
icon: <WifiOff className="h-5 w-5 text-neutral-400" />,
|
||||
text: status || "Unknown",
|
||||
colorClass: "text-neutral-400",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, text, colorClass } = getStatusInfo();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 bg-neutral-800 p-4 border-1 border-b-neutral-700 border-r-neutral-700 border-t-neutral-950 border-l-neutral-950 rounded-sm">
|
||||
{icon}
|
||||
<span className={`text-sm font-medium ${colorClass}`}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface FilterProps {
|
||||
onChange: (filter: string) => void;
|
||||
@@ -8,13 +8,16 @@ interface FilterProps {
|
||||
export const Filter: React.FC<FilterProps> = ({ onChange, value }) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="filter" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label
|
||||
htmlFor="filter"
|
||||
className="block text-sm font-medium text-neutral-700 mb-1"
|
||||
>
|
||||
Filter Packets
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filter"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
className="w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Filter by type, sender, etc."
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Info, AlertTriangle, AlertCircle } from "lucide-react";
|
||||
|
||||
interface InfoMessageProps {
|
||||
message: string;
|
||||
type?: 'info' | 'warning' | 'error';
|
||||
type?: "info" | "warning" | "error";
|
||||
}
|
||||
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
message,
|
||||
type = 'info'
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
message,
|
||||
type = "info",
|
||||
}) => {
|
||||
const getBgColor = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'bg-yellow-50 border-yellow-200';
|
||||
case 'error':
|
||||
return 'bg-red-50 border-red-200';
|
||||
case 'info':
|
||||
case "warning":
|
||||
return "bg-neutral-700 border-neutral-600";
|
||||
case "error":
|
||||
return "bg-neutral-800 border-neutral-700";
|
||||
case "info":
|
||||
default:
|
||||
return 'bg-blue-50 border-blue-200';
|
||||
return "bg-neutral-800 border-neutral-700";
|
||||
}
|
||||
};
|
||||
|
||||
const getTextColor = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'text-yellow-800';
|
||||
case 'error':
|
||||
return 'text-red-800';
|
||||
case 'info':
|
||||
case "warning":
|
||||
return "text-amber-400";
|
||||
case "error":
|
||||
return "text-red-400";
|
||||
case "info":
|
||||
default:
|
||||
return 'text-blue-800';
|
||||
return "text-neutral-300";
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-5 w-5 text-amber-400" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-5 w-5 text-red-400" />;
|
||||
case "info":
|
||||
default:
|
||||
return <Info className="h-5 w-5 text-neutral-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`p-3 mb-4 rounded border ${getBgColor()}`}>
|
||||
<p className={`text-sm ${getTextColor()}`}>{message}</p>
|
||||
<div className={`p-3 mb-4 rounded border ${getBgColor()} shadow-inner`}>
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 mt-0.5 mr-3">{getIcon()}</div>
|
||||
<p className={`text-sm ${getTextColor()}`}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Packet } from "../lib/types";
|
||||
|
||||
interface MessageDisplayProps {
|
||||
message: any; // Will be properly typed once we have the protobuf structures
|
||||
message: Packet;
|
||||
}
|
||||
|
||||
export const MessageDisplay: React.FC<MessageDisplayProps> = ({ message }) => {
|
||||
// The data structure will be refined as we integrate with the protobuf definitions
|
||||
const { data } = message;
|
||||
|
||||
const getMessageContent = () => {
|
||||
if (data.text_message) {
|
||||
return data.text_message;
|
||||
} else if (data.position) {
|
||||
return `Position: ${data.position.latitude}, ${data.position.longitude}`;
|
||||
} else if (data.node_info) {
|
||||
return `Node Info: ${data.node_info.longName || data.node_info.shortName}`;
|
||||
} else if (data.telemetry) {
|
||||
return "Telemetry data";
|
||||
} else if (data.decode_error) {
|
||||
return `Error: ${data.decode_error}`;
|
||||
}
|
||||
return "Unknown message type";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 border rounded shadow-sm bg-white">
|
||||
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="font-medium">{message.from || 'Unknown'}</span>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{message.timestamp ? new Date(message.timestamp).toLocaleString() : 'No timestamp'}
|
||||
<span className="font-medium text-neutral-200">
|
||||
From: {data.from || "Unknown"}
|
||||
</span>
|
||||
<span className="text-neutral-400 text-sm">
|
||||
ID: {data.id || "No ID"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
{message.text || 'No content'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
ID: {message.id || 'No ID'}
|
||||
<div className="mb-2 text-neutral-300">{getMessageContent()}</div>
|
||||
<div className="mt-3 flex justify-between items-center">
|
||||
<span className="text-xs text-neutral-500">
|
||||
Channel: {message.info.channel}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500">Type: {data.port_num}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ConnectionStatus } from "./ConnectionStatus";
|
||||
import { Separator } from "./Separator";
|
||||
import { SITE_TITLE } from "../lib/config";
|
||||
|
||||
interface NavProps {
|
||||
connectionStatus: string;
|
||||
}
|
||||
|
||||
export const Nav: React.FC<NavProps> = ({ connectionStatus }) => {
|
||||
return (
|
||||
<aside className="w-64 text-neutral-100 h-screen fixed left-0 top-0 flex flex-col">
|
||||
{/* Logo section */}
|
||||
<div className="p-4 mb-2">
|
||||
<h1 className="text-xl font-bold">{SITE_TITLE}</h1>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<nav className="flex-1 pt-4">
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link
|
||||
to="/"
|
||||
className="block px-4 py-2 hover:bg-neutral-800 transition-colors"
|
||||
activeProps={{
|
||||
className:
|
||||
"block px-4 py-2 bg-neutral-800 border-l-4 border-neutral-500",
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="/packets"
|
||||
className="block px-4 py-2 hover:bg-neutral-800 transition-colors"
|
||||
activeProps={{
|
||||
className:
|
||||
"block px-4 py-2 bg-neutral-800 border-l-4 border-neutral-500",
|
||||
}}
|
||||
>
|
||||
Packets
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="px-4 py-2 pb-6">
|
||||
<ConnectionStatus status={connectionStatus} />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface PacketDetailsProps {
|
||||
packet: any; // Will be properly typed once we have the protobuf structures
|
||||
@@ -6,9 +6,9 @@ interface PacketDetailsProps {
|
||||
|
||||
export const PacketDetails: React.FC<PacketDetailsProps> = ({ packet }) => {
|
||||
return (
|
||||
<div className="p-4 border rounded bg-gray-50">
|
||||
<div className="p-4 border rounded bg-neutral-50">
|
||||
<h3 className="text-lg font-semibold mb-2">Packet Details</h3>
|
||||
<pre className="text-xs overflow-auto p-2 bg-gray-100 rounded">
|
||||
<pre className="text-xs overflow-auto p-2 bg-neutral-100 rounded">
|
||||
{JSON.stringify(packet, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useAppSelector } from '../hooks';
|
||||
import React from "react";
|
||||
import { useAppSelector } from "../hooks";
|
||||
|
||||
export const PacketList: React.FC = () => {
|
||||
const { packets, loading, error } = useAppSelector(state => state.packets);
|
||||
const { packets, loading, error } = useAppSelector((state) => state.packets);
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading...</div>;
|
||||
return <div className="p-4 text-neutral-300">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-500">Error: {error}</div>;
|
||||
return <div className="p-4 text-red-400">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (packets.length === 0) {
|
||||
return <div className="p-4">No packets received yet</div>;
|
||||
return <div className="p-4 text-neutral-400">No packets received yet</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-bold mb-4">Received Packets</h2>
|
||||
<h2 className="text-xl font-bold mb-4 text-neutral-200">
|
||||
Received Packets
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{packets.map(packet => (
|
||||
<li key={packet.id} className="p-2 border rounded">
|
||||
{packets.map((packet) => (
|
||||
<li
|
||||
key={packet.id}
|
||||
className="p-3 border border-neutral-700 rounded bg-neutral-900 shadow-inner text-neutral-300 hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
{packet.id}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
interface SeparatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Separator: React.FC<SeparatorProps> = ({ className = "" }) => {
|
||||
return (
|
||||
<div
|
||||
className={`mx-4 h-px border-t-1 border-t-neutral-900 border-b-1 border-b-neutral-700/80 my-3 ${className}`}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
@@ -3,3 +3,6 @@ export * from './MessageDisplay';
|
||||
export * from './PacketDetails';
|
||||
export * from './Filter';
|
||||
export * from './InfoMessage';
|
||||
export * from './ConnectionStatus';
|
||||
export * from './Nav';
|
||||
export * from './Separator';
|
||||
@@ -7,6 +7,9 @@ export const IS_DEV = import.meta.env.DEV;
|
||||
export const IS_PROD = import.meta.env.PROD;
|
||||
export const APP_ENV = import.meta.env.VITE_APP_ENV || 'development';
|
||||
|
||||
// Site configuration
|
||||
export const SITE_TITLE = import.meta.env.VITE_SITE_TITLE || 'ERSN Mesh';
|
||||
|
||||
// API URL configuration
|
||||
const getApiBaseUrl = (): string => {
|
||||
// In production, use the same domain (empty string base URL)
|
||||
@@ -24,4 +27,4 @@ export const API_BASE_URL = getApiBaseUrl();
|
||||
export const API_ENDPOINTS = {
|
||||
STREAM: `${API_BASE_URL}/api/stream`,
|
||||
RECENT_PACKETS: `${API_BASE_URL}/api/packets/recent`,
|
||||
};
|
||||
};
|
||||
+33
-30
@@ -1,36 +1,39 @@
|
||||
import { Link, Outlet } from '@tanstack/react-router';
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Nav } from "../components";
|
||||
import { streamPackets, StreamEvent } from "../lib/api";
|
||||
|
||||
export default function Root() {
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<string>("Connecting...");
|
||||
|
||||
useEffect(() => {
|
||||
// Set up Server-Sent Events connection
|
||||
const cleanup = streamPackets(
|
||||
// Event handler for all event types
|
||||
(event: StreamEvent) => {
|
||||
if (event.type === "info") {
|
||||
// Handle info events (connection status, etc.)
|
||||
setConnectionStatus(event.data);
|
||||
}
|
||||
},
|
||||
// On error
|
||||
() => {
|
||||
setConnectionStatus("Connection error. Reconnecting...");
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up connection when component unmounts
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<header className="bg-blue-600 text-white shadow-md">
|
||||
<div className="container mx-auto px-4 py-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">Meshstream</h1>
|
||||
<nav className="space-x-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/packets"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
}}
|
||||
>
|
||||
Packets
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="container mx-auto px-4 py-6">
|
||||
<div className="flex min-h-screen bg-neutral-800">
|
||||
{/* Sidebar Navigation */}
|
||||
<Nav connectionStatus={connectionStatus} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="ml-64 flex-1 p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
+42
-11
@@ -1,17 +1,48 @@
|
||||
import { InfoMessage, Separator } from "../components";
|
||||
import { SITE_TITLE } from "../lib/config";
|
||||
|
||||
export function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Welcome to Meshstream</h2>
|
||||
<p className="mb-4">
|
||||
This application provides a real-time view of Meshtastic network traffic.
|
||||
</p>
|
||||
<div className="bg-blue-100 p-4 rounded">
|
||||
<h3 className="text-lg font-semibold mb-2">Getting Started</h3>
|
||||
<p>
|
||||
Click on the <strong>Packets</strong> link in the navigation to view incoming
|
||||
messages from the Meshtastic network.
|
||||
<div className="bg-neutral-700 rounded-lg shadow-inner">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neutral-100">
|
||||
Welcome to {SITE_TITLE}
|
||||
</h2>
|
||||
<p className="mb-4 text-neutral-200">
|
||||
This application provides a real-time view of Meshtastic network
|
||||
traffic.
|
||||
</p>
|
||||
|
||||
<InfoMessage
|
||||
message="Click on the Packets link in the navigation to view incoming messages from the Meshtastic network."
|
||||
type="info"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-6" />
|
||||
|
||||
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-neutral-800 border border-neutral-700 p-5 rounded shadow-inner">
|
||||
<h3 className="text-lg font-semibold mb-3 text-neutral-200">
|
||||
About Meshtastic
|
||||
</h3>
|
||||
<p className="text-neutral-300">
|
||||
Meshtastic is an open source, off-grid, decentralized mesh
|
||||
communication platform. It allows devices to communicate without
|
||||
cellular service or internet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-800 border border-neutral-700 p-5 rounded shadow-inner">
|
||||
<h3 className="text-lg font-semibold mb-3 text-neutral-200">
|
||||
Data Privacy
|
||||
</h3>
|
||||
<p className="text-neutral-300">
|
||||
All data is processed locally. Position data on public servers has
|
||||
reduced precision for privacy protection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+26
-32
@@ -1,53 +1,47 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch } from '../hooks';
|
||||
import { PacketList } from '../components/PacketList';
|
||||
import { InfoMessage } from '../components/InfoMessage';
|
||||
import { addPacket } from '../store/slices/packetSlice';
|
||||
import { streamPackets, StreamEvent } from '../lib/api';
|
||||
import { useEffect } from "react";
|
||||
import { useAppDispatch } from "../hooks";
|
||||
import { PacketList } from "../components/PacketList";
|
||||
import { InfoMessage, Separator } from "../components";
|
||||
import { addPacket } from "../store/slices/packetSlice";
|
||||
import { streamPackets, StreamEvent } from "../lib/api";
|
||||
|
||||
export function PacketsRoute() {
|
||||
const dispatch = useAppDispatch();
|
||||
const [connectionStatus, setConnectionStatus] = useState<string>('Connecting...');
|
||||
|
||||
useEffect(() => {
|
||||
// Set up Server-Sent Events connection using our API utility
|
||||
const cleanup = streamPackets(
|
||||
// Event handler for all event types
|
||||
(event: StreamEvent) => {
|
||||
if (event.type === 'info') {
|
||||
// Handle info events (connection status, etc.)
|
||||
setConnectionStatus(event.data);
|
||||
} else if (event.type === 'message') {
|
||||
if (event.type === "message") {
|
||||
// Handle message events (actual packet data)
|
||||
dispatch(addPacket(event.data));
|
||||
}
|
||||
},
|
||||
// On error
|
||||
() => {
|
||||
setConnectionStatus('Connection error. Reconnecting...');
|
||||
console.error('EventSource failed, reconnecting...');
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Clean up connection when component unmounts
|
||||
return cleanup;
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Mesh Network Packets</h2>
|
||||
|
||||
{/* Connection status indicator */}
|
||||
<InfoMessage
|
||||
message={`Status: ${connectionStatus}`}
|
||||
type={connectionStatus.includes('error') ? 'error' : 'info'}
|
||||
/>
|
||||
|
||||
<p className="mb-4">
|
||||
This page displays real-time packets from the Meshtastic mesh network.
|
||||
</p>
|
||||
|
||||
<PacketList />
|
||||
<div className="bg-neutral-700 rounded-lg shadow-inner">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neutral-100">
|
||||
Mesh Network Packets
|
||||
</h2>
|
||||
|
||||
<InfoMessage
|
||||
message="This page displays real-time packets from the Meshtastic mesh network."
|
||||
type="info"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="p-6">
|
||||
<PacketList />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link, Outlet } from '@tanstack/react-router';
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
export function Root() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<div className="min-h-screen bg-neutral-100">
|
||||
<header className="bg-blue-600 text-white shadow-md">
|
||||
<div className="container mx-auto px-4 py-3">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -12,7 +12,7 @@ export function Root() {
|
||||
to="/"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
className: "font-bold underline",
|
||||
}}
|
||||
>
|
||||
Home
|
||||
@@ -21,7 +21,7 @@ export function Root() {
|
||||
to="/packets"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
className: "font-bold underline",
|
||||
}}
|
||||
>
|
||||
Packets
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Additional custom styles can be added here */
|
||||
/* Set default background and text colors */
|
||||
@layer base {
|
||||
html, body {
|
||||
@apply bg-neutral-800;
|
||||
@apply text-neutral-200;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user