From 8e6c04571420783d643c98b49226c9615f9f2e2a Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 20:02:04 -0500 Subject: [PATCH 1/2] feat(ui): make the app mobile-responsive The redesign was built desktop-first with no media queries below 800px. This adds a three-tier responsive treatment (mobile <=640 / tablet 641-1024 / desktop >1024, matching Tailwind sm/md/lg) across the app shell and the three main pages. - Navbar.tsx: useState-driven hamburger that exposes all primary + more links in a single flat panel on changes). - app/new-contact/page.tsx: page padding; outer layout single-col below xl; callsign mega-input scales 32->56px; lookup card grid restructures with Verified chip spanning columns on mobile; action footer reverses with Save QSO on top. Verified: lint, typecheck, build all clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/dashboard/page.tsx | 270 ++++++++++++++++++++++------------- src/app/new-contact/page.tsx | 30 ++-- src/app/page.tsx | 31 ++-- src/components/Navbar.tsx | 45 +++++- 4 files changed, 238 insertions(+), 138 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 112c8fb..b903c7f 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { Loader2, Plus, Search } from 'lucide-react'; @@ -129,6 +129,17 @@ function formatUtc(date: string) { return `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')} UTC`; } +function MobileCardRow({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ + {label} + + {children} +
+ ); +} + export default function DashboardPage() { const [contacts, setContacts] = useState([]); const [loading, setLoading] = useState(true); @@ -290,7 +301,7 @@ export default function DashboardPage() {
-
+
@@ -343,19 +354,16 @@ export default function DashboardPage() {
{/* Map + Quick Log */} -
+
{error && (
{error}
)} -
- -
+
+ +
Worldmap @@ -367,7 +375,7 @@ export default function DashboardPage() {
- +

Quick log

@@ -428,7 +436,7 @@ export default function DashboardPage() { onChange={setBandRange} />
-
+
{BAND_ORDER.map((band) => { const count = bandActivity[band] ?? 0; const filled = activityBars(count); @@ -437,7 +445,7 @@ export default function DashboardPage() {
{/* Log + DXpeditions */} -
+
-
-
- +
+
+ setTableSearch(e.target.value)} placeholder="Search callsign, name, grid, frequency…" - className="flex-1 bg-transparent border-0 outline-none text-fg text-[15px] placeholder:text-fg-3" + className="flex-1 min-w-0 bg-transparent border-0 outline-none text-fg text-[15px] placeholder:text-fg-3" /> /
@@ -496,105 +501,168 @@ export default function DashboardPage() {

) : ( - - - - Callsign - When - Band / Mode - Freq - RST - Operator - QSL - - - + <> + {/* Desktop: classic table */} +
+
+ + + Callsign + When + Band / Mode + Freq + RST + Operator + QSL + + + + {loading ? ( + + +
+ + Loading contacts… +
+
+
+ ) : filteredContacts.length === 0 ? ( + + + No contacts match those filters. + + + ) : ( + filteredContacts.map((contact) => ( + handleContactClick(contact)} + > + + + {contact.callsign} + + + + {formatUtc(contact.datetime)} +
+ + {formatRelativeTime(contact.datetime)} + +
+ +
+ {contact.band} + {contact.mode} +
+
+ + + {contact.frequency} + + + + + {contact.rst_sent ?? '-'} / {contact.rst_received ?? '-'} + + + + {contact.name ?? '—'} + {contact.qth ? ( + · {contact.qth} + ) : null} + + +
e.stopPropagation()}> + {qslChip(contact)} + + fetchContacts()} + size="sm" + /> + + +
+
+
+ )) + )} +
+
+
+ + {/* Mobile: stacked QSO cards */} +
{loading ? ( - - -
- - Loading contacts… -
-
-
+
+ + Loading contacts… +
) : filteredContacts.length === 0 ? ( - - - No contacts match those filters. - - +
+ No contacts match those filters. +
) : ( filteredContacts.map((contact) => ( - handleContactClick(contact)} + className="rounded-xl border border-line bg-bg-1 p-3.5 text-left cursor-pointer hover:border-line-hi transition-colors" > - - +
+ {contact.callsign} - - - {formatUtc(contact.datetime)} -
- - {formatRelativeTime(contact.datetime)} + {qslChip(contact)} +
+ + + {formatUtc(contact.datetime)}{' '} + · {formatRelativeTime(contact.datetime)} -
- -
+ + + {contact.band} {contact.mode} -
-
- - - {contact.frequency} - - + + + {contact.frequency} + + {contact.rst_sent ?? '-'} / {contact.rst_received ?? '-'} - - - {contact.name ?? '—'} - {contact.qth ? ( - · {contact.qth} - ) : null} - - -
e.stopPropagation()}> - {qslChip(contact)} - - fetchContacts()} - size="sm" - /> - - -
-
-
+ + + + {contact.name ?? '—'} + {contact.qth ? ( + · {contact.qth} + ) : null} + + + )) )} - - +
+ )} {pagination.pages > 1 && ( diff --git a/src/app/new-contact/page.tsx b/src/app/new-contact/page.tsx index db6dbb0..6290753 100644 --- a/src/app/new-contact/page.tsx +++ b/src/app/new-contact/page.tsx @@ -424,7 +424,7 @@ export default function NewContactPage() {
-
+
-
+
{/* LEFT — form */}
{/* Callsign hero */} - + Verified @@ -550,7 +546,7 @@ export default function NewContactPage() { {/* Band & Mode */} - +

Band & Mode

{formData.frequency ? ( @@ -643,7 +639,7 @@ export default function NewContactPage() { {/* Signal report & details */} - +

Signal report & details

@@ -792,12 +788,12 @@ export default function NewContactPage() {
) : null} -
- -
- -
{/* Preview mockup */} -
+
-
+
@@ -300,7 +300,7 @@ export default function Home() { {/* Features */}
Built for operators @@ -340,9 +340,8 @@ export default function Home() { {/* CTA */}
Free to self-host. Open source. Ready when you are.

-
- - + -
+ {mobileOpen && ( + + )} + +
{user?.callsign ? ( From b417f5f07fca60d03c8e2e9b6a13ecfd9fd006c1 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 20:06:36 -0500 Subject: [PATCH 2/2] fix(ui): stack EditContactDialog footer buttons cleanly on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The footer was layering DialogFooter's default `flex-col-reverse` with a local `flex justify-between`, producing an awkward Cancel | Save row above an orphaned Delete QSO button on narrow viewports. Drop the wrapper `
` around the AlertDialog and let it become a direct DialogFooter child. On mobile, every button stretches `w-full` and stacks in the order Save Changes → Cancel → Delete QSO (the destructive action lives at the bottom, away from the primary tap target). On `sm` and up, Delete sits on the left and Cancel/Save group sits on the right via `sm:justify-between` + `sm:ml-auto` (works the same with or without onDelete). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/EditContactDialog.tsx | 83 ++++++++++++++-------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/components/EditContactDialog.tsx b/src/components/EditContactDialog.tsx index 8931365..274b7d1 100644 --- a/src/components/EditContactDialog.tsx +++ b/src/components/EditContactDialog.tsx @@ -463,53 +463,52 @@ export default function EditContactDialog({ contact, isOpen, onClose, onSave, on />
- -
- {onDelete && ( - - - + + + + Are you sure? + + Are you sure you want to delete this QSO with {contact?.callsign}? This action cannot be undone. + + + {deleteError && ( + + + {deleteError} + + )} + + Cancel + {deleteLoading && } Delete QSO - - - - - Are you sure? - - Are you sure you want to delete this QSO with {contact?.callsign}? This action cannot be undone. - - - {deleteError && ( - - - {deleteError} - - )} - - Cancel - - {deleteLoading && } - Delete QSO - - - - - )} -
-
- -