Jelajahi Sumber

Improve WiFi page mobile layout and fix desktop menu close

- Compact card layout: replace CardHeader/CardTitle with inline headings
- Status items use flex-wrap with Host right-aligned on desktop
- Tighter spacing throughout for mobile viewports
- Icon-only refresh button for network scan
- Move Raspberry Pi note inside status card
- Desktop hamburger menu now closes on WiFi Setup click

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tuanchris 4 bulan lalu
induk
melakukan
a5e420dacf
2 mengubah file dengan 114 tambahan dan 106 penghapusan
  1. 6 2
      frontend/src/components/layout/Layout.tsx
  2. 108 104
      frontend/src/pages/WiFiSetupPage.tsx

+ 6 - 2
frontend/src/components/layout/Layout.tsx

@@ -161,6 +161,7 @@ export function Layout() {
 
   // Mobile menu state
   const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+  const [isDesktopMenuOpen, setIsDesktopMenuOpen] = useState(false)
 
   // Logs drawer state
   const [isLogsOpen, setIsLogsOpen] = useState(false)
@@ -1541,7 +1542,7 @@ export function Layout() {
                 </span>
               </Link>
             )}
-            <Popover>
+            <Popover open={isDesktopMenuOpen} onOpenChange={setIsDesktopMenuOpen}>
               <PopoverTrigger asChild>
                 <Button
                   variant="ghost"
@@ -1571,7 +1572,10 @@ export function Layout() {
                     View Logs
                   </button>
                   <button
-                    onClick={() => navigate('/wifi-setup')}
+                    onClick={() => {
+                      navigate('/wifi-setup')
+                      setIsDesktopMenuOpen(false)
+                    }}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors"
                   >
                     <span className="material-icons-outlined text-xl">wifi</span>

+ 108 - 104
frontend/src/pages/WiFiSetupPage.tsx

@@ -4,7 +4,7 @@ import { apiClient } from '@/lib/apiClient'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
+import { Card, CardContent } from '@/components/ui/card'
 import { Alert, AlertDescription } from '@/components/ui/alert'
 import { Badge } from '@/components/ui/badge'
 import { Separator } from '@/components/ui/separator'
@@ -170,7 +170,7 @@ export function WiFiSetupPage() {
   }
 
   return (
-    <div className="container max-w-lg mx-auto px-4 py-6 space-y-6">
+    <div className="container max-w-lg mx-auto px-3 py-4 space-y-3">
       {/* Hotspot Welcome Banner */}
       {isHotspotMode && (
         <Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950/30 dark:border-blue-800">
@@ -185,138 +185,142 @@ export function WiFiSetupPage() {
 
       {/* Current Status */}
       <Card>
-        <CardHeader className="pb-3">
-          <CardTitle className="text-lg flex items-center gap-2">
-            <span className="material-icons-outlined text-muted-foreground">info</span>
-            WiFi Status
-          </CardTitle>
-        </CardHeader>
-        <CardContent>
+        <CardContent className="pt-4 pb-3 px-4">
+          <div className="flex items-center gap-2 mb-2">
+            <span className="material-icons-outlined text-base text-muted-foreground">info</span>
+            <span className="font-semibold text-sm">WiFi Status</span>
+          </div>
           {status ? (
-            <div className="grid grid-cols-2 gap-3 text-sm">
-              <div>
-                <p className="text-muted-foreground">Mode</p>
-                <Badge variant={isHotspotMode ? 'secondary' : 'default'}>
-                  {status.mode === 'hotspot' ? 'Hotspot' :
-                   status.mode === 'client' ? 'Connected' : status.mode}
-                </Badge>
-              </div>
-              {status.ssid && (
-                <div>
-                  <p className="text-muted-foreground">Network</p>
-                  <p className="font-medium">{status.ssid}</p>
-                </div>
-              )}
-              {status.ip && (
-                <div>
-                  <p className="text-muted-foreground">IP Address</p>
-                  <p className="font-mono text-xs">{status.ip}</p>
+            <div className="flex flex-wrap items-center justify-between gap-y-1 text-sm">
+              <div className="flex flex-wrap items-center gap-x-5 gap-y-1">
+                <div className="flex items-center gap-1.5">
+                  <span className="text-muted-foreground text-xs">Mode</span>
+                  <Badge variant={isHotspotMode ? 'secondary' : 'default'} className="text-xs px-1.5 py-0">
+                    {status.mode === 'hotspot' ? 'Hotspot' :
+                     status.mode === 'client' ? 'Connected' : status.mode}
+                  </Badge>
                 </div>
-              )}
-              <div>
-                <p className="text-muted-foreground">Hostname</p>
-                <p className="font-mono text-xs">{status.hostname}.local</p>
+                {status.ssid && (
+                  <div className="flex items-center gap-1.5">
+                    <span className="text-muted-foreground text-xs">Network</span>
+                    <span className="font-medium text-xs">{status.ssid}</span>
+                  </div>
+                )}
+                {status.ip && (
+                  <div className="flex items-center gap-1.5">
+                    <span className="text-muted-foreground text-xs">IP</span>
+                    <span className="font-mono text-xs">{status.ip}</span>
+                  </div>
+                )}
+              </div>
+              <div className="flex items-center gap-1.5">
+                <span className="text-muted-foreground text-xs">Host</span>
+                <span className="font-mono text-xs">{status.hostname}.local</span>
               </div>
             </div>
           ) : (
             <p className="text-sm text-muted-foreground">Loading status...</p>
           )}
+          {/* Raspberry Pi note */}
+          {!isHotspotMode && (
+            <p className="text-xs text-muted-foreground mt-2">
+              WiFi management requires a Raspberry Pi.
+            </p>
+          )}
         </CardContent>
       </Card>
 
       {/* Available Networks */}
       <Card>
-        <CardHeader className="pb-3">
-          <div className="flex items-center justify-between">
-            <div>
-              <CardTitle className="text-lg flex items-center gap-2">
-                <span className="material-icons-outlined text-muted-foreground">wifi_find</span>
-                Available Networks
-              </CardTitle>
-              <CardDescription>
+        <CardContent className="pt-4 pb-2 px-4">
+          <div className="flex items-center justify-between mb-2">
+            <div className="flex items-center gap-2">
+              <span className="material-icons-outlined text-base text-muted-foreground">wifi_find</span>
+              <span className="font-semibold text-sm">Networks</span>
+              <span className="text-xs text-muted-foreground">
                 {networks.length > 0
-                  ? `${networks.length} network${networks.length !== 1 ? 's' : ''} found`
-                  : 'Scanning...'}
-              </CardDescription>
+                  ? `(${networks.length})`
+                  : ''}
+              </span>
             </div>
             <Button
-              variant="outline"
+              variant="ghost"
               size="sm"
+              className="h-7 w-7 p-0"
               onClick={scanNetworks}
               disabled={isScanning}
             >
-              <span className={`material-icons text-sm mr-1 ${isScanning ? 'animate-spin' : ''}`}>
+              <span className={`material-icons text-base ${isScanning ? 'animate-spin' : ''}`}>
                 refresh
               </span>
-              Scan
             </Button>
           </div>
-        </CardHeader>
-        <CardContent className="space-y-1">
-          {networks.length === 0 && isScanning && (
-            <p className="text-sm text-muted-foreground text-center py-4">
-              Scanning for networks...
-            </p>
-          )}
-          {networks.length === 0 && !isScanning && (
-            <p className="text-sm text-muted-foreground text-center py-4">
-              No networks found. Try scanning again.
-            </p>
-          )}
-          {networks.map((network) => (
-            <button
-              key={network.ssid}
-              onClick={() => openConnectDialog(network)}
-              className={`w-full flex items-center gap-3 p-3 rounded-lg text-left transition-colors hover:bg-muted/50
-                ${network.active ? 'bg-green-50 dark:bg-green-950/20' : ''}`}
-            >
-              <SignalIcon signal={network.signal} />
-              <div className="flex-1 min-w-0">
-                <p className="font-medium truncate">{network.ssid}</p>
-                <p className="text-xs text-muted-foreground">
-                  {network.security}
-                  {network.saved && ' · Saved'}
-                  {network.active && ' · Connected'}
-                </p>
-              </div>
-              <span className="text-xs text-muted-foreground">{network.signal}%</span>
-              {network.security !== 'Open' && (
-                <span className="material-icons text-sm text-muted-foreground">lock</span>
-              )}
-            </button>
-          ))}
+          <div className="space-y-0.5">
+            {networks.length === 0 && isScanning && (
+              <p className="text-sm text-muted-foreground text-center py-3">
+                Scanning for networks...
+              </p>
+            )}
+            {networks.length === 0 && !isScanning && (
+              <p className="text-sm text-muted-foreground text-center py-3">
+                No networks found. Try scanning again.
+              </p>
+            )}
+            {networks.map((network) => (
+              <button
+                key={network.ssid}
+                onClick={() => openConnectDialog(network)}
+                className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-left transition-colors hover:bg-muted/50
+                  ${network.active ? 'bg-green-50 dark:bg-green-950/20' : ''}`}
+              >
+                <SignalIcon signal={network.signal} />
+                <div className="flex-1 min-w-0">
+                  <p className="font-medium text-sm truncate">{network.ssid}</p>
+                  <p className="text-xs text-muted-foreground">
+                    {network.security}
+                    {network.saved && ' · Saved'}
+                    {network.active && ' · Connected'}
+                  </p>
+                </div>
+                <span className="text-xs text-muted-foreground">{network.signal}%</span>
+                {network.security !== 'Open' && (
+                  <span className="material-icons text-sm text-muted-foreground">lock</span>
+                )}
+              </button>
+            ))}
+          </div>
         </CardContent>
       </Card>
 
       {/* Saved Networks */}
       {savedConnections.length > 0 && (
         <Card>
-          <CardHeader className="pb-3">
-            <CardTitle className="text-lg flex items-center gap-2">
-              <span className="material-icons-outlined text-muted-foreground">bookmark</span>
-              Saved Networks
-            </CardTitle>
-          </CardHeader>
-          <CardContent className="space-y-1">
-            {savedConnections.map((con) => (
-              <div
-                key={con.name}
-                className="flex items-center justify-between p-3 rounded-lg hover:bg-muted/50"
-              >
-                <div className="flex items-center gap-3">
-                  <span className="material-icons text-muted-foreground">wifi</span>
-                  <span className="font-medium">{con.ssid}</span>
-                </div>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  onClick={() => handleForget(con.ssid)}
-                  className="text-destructive hover:text-destructive"
+          <CardContent className="pt-4 pb-2 px-4">
+            <div className="flex items-center gap-2 mb-2">
+              <span className="material-icons-outlined text-base text-muted-foreground">bookmark</span>
+              <span className="font-semibold text-sm">Saved Networks</span>
+            </div>
+            <div className="space-y-0.5">
+              {savedConnections.map((con) => (
+                <div
+                  key={con.name}
+                  className="flex items-center justify-between px-2.5 py-2 rounded-lg hover:bg-muted/50"
                 >
-                  <span className="material-icons text-sm">delete</span>
-                </Button>
-              </div>
-            ))}
+                  <div className="flex items-center gap-2.5">
+                    <span className="material-icons text-muted-foreground text-lg">wifi</span>
+                    <span className="font-medium text-sm">{con.ssid}</span>
+                  </div>
+                  <Button
+                    variant="ghost"
+                    size="sm"
+                    className="h-7 w-7 p-0"
+                    onClick={() => handleForget(con.ssid)}
+                  >
+                    <span className="material-icons text-sm text-destructive">delete</span>
+                  </Button>
+                </div>
+              ))}
+            </div>
           </CardContent>
         </Card>
       )}