Public Access
1
0

feat: add workspace account page and mobile app shell

Normalize the UI with shared primitives, add a workspace-backed account surface, and improve authenticated mobile navigation, map behavior, and dashboard browsing.
This commit is contained in:
pguerrerox
2026-05-07 17:40:10 +00:00
parent d4bce92872
commit 1f7737e5cb
24 changed files with 1397 additions and 489 deletions
+18 -21
View File
@@ -5,6 +5,7 @@ import { listBusinesses, listBusinessesForJobs } from '../lib/database';
import { cleanMapOptions } from '../lib/map-styles';
import type { Business } from '../types';
import type { AppUser } from '../../shared/types';
import { Alert, Badge, EmptyState } from './ui';
interface MapViewProps {
user: AppUser;
@@ -101,17 +102,15 @@ export function MapView({ user, jobIds }: MapViewProps) {
if (businesses.length === 0) {
return (
<div className="flex flex-1 items-center justify-center bg-stone-50 p-8">
<div className="w-full max-w-lg rounded-3xl border border-stone-200 bg-white p-8 text-center shadow-sm">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-stone-100 text-stone-500">
<MapPin className="h-6 w-6" />
</div>
<h2 className="mt-4 text-2xl font-bold text-stone-900">No leads to show on the map</h2>
<p className="mt-3 text-sm text-stone-600">
{selectedJobCount > 0
<div className="w-full max-w-lg">
<EmptyState
icon={MapPin}
title="No leads to show on the map"
description={selectedJobCount > 0
? 'The selected research jobs do not have saved map results yet. Try completed jobs or run the research again.'
: 'No saved leads are available yet. Run a research job to populate the map.'}
</p>
{error && <p className="mt-4 text-sm font-medium text-red-700">{error}</p>}
/>
{error ? <Alert variant="error" className="mt-4">{error}</Alert> : null}
</div>
</div>
);
@@ -120,9 +119,7 @@ export function MapView({ user, jobIds }: MapViewProps) {
return (
<div className="relative flex-1 bg-stone-100">
{error && (
<div className="absolute left-8 top-8 z-10 rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700 shadow-lg">
{error}
</div>
<Alert variant="error" className="absolute left-4 right-4 top-4 z-10 shadow-lg lg:left-8 lg:right-auto lg:top-8 lg:max-w-md">{error}</Alert>
)}
<Map
@@ -154,7 +151,7 @@ export function MapView({ user, jobIds }: MapViewProps) {
<InfoWindow position={{ lat: selected.latitude, lng: selected.longitude }} onCloseClick={() => setSelected(null)}>
<div className="max-w-[280px] space-y-3 p-2">
<header>
<h3 className="text-base font-bold leading-tight text-stone-900">{selected.name}</h3>
<h3 className="text-base font-semibold leading-tight text-stone-950">{selected.name}</h3>
<div className="mt-1 flex items-center gap-1 text-xs text-stone-500">
<MapPin className="h-3 w-3" />
<span className="truncate">{selected.address}</span>
@@ -162,14 +159,14 @@ export function MapView({ user, jobIds }: MapViewProps) {
</header>
<div className="flex items-center gap-3 border-y border-stone-100 py-2">
<div className="flex items-center gap-1 text-sm font-bold text-amber-600">
<div className="flex items-center gap-1 text-sm font-semibold text-amber-600">
<Star className="h-4 w-4 fill-amber-500 text-amber-500" />
{selected.rating || 'N/A'}
<span className="text-xs font-normal text-stone-400">({selected.reviewCount || 0})</span>
</div>
<span className="rounded-full bg-stone-100 px-2 py-0.5 text-xs font-medium text-stone-600">
<Badge>
{selected.category || 'Uncategorized'}
</span>
</Badge>
</div>
<div className="flex items-center gap-2">
@@ -178,7 +175,7 @@ export function MapView({ user, jobIds }: MapViewProps) {
href={selected.website}
target="_blank"
rel="noopener"
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-emerald-600 py-2 text-xs font-semibold text-white transition-all hover:bg-emerald-700"
className="inline-flex h-9 flex-1 items-center justify-center gap-2 rounded-xl bg-emerald-600 px-3 text-xs font-semibold text-white transition hover:bg-emerald-700"
>
<Globe className="h-3.5 w-3.5" />
Website
@@ -187,7 +184,7 @@ export function MapView({ user, jobIds }: MapViewProps) {
{selected.phone && (
<a
href={`tel:${selected.phone}`}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-stone-900 py-2 text-xs font-semibold text-white transition-all hover:bg-stone-800"
className="inline-flex h-9 flex-1 items-center justify-center gap-2 rounded-xl border border-stone-200 bg-white px-3 text-xs font-semibold text-stone-700 transition hover:bg-stone-50"
>
<Phone className="h-3.5 w-3.5" />
Call
@@ -209,8 +206,8 @@ export function MapView({ user, jobIds }: MapViewProps) {
)}
</Map>
<div className="absolute bottom-8 left-8 z-10 max-w-xs rounded-2xl border border-white/20 bg-white/90 p-4 shadow-xl backdrop-blur-sm">
<h4 className="mb-2 text-sm font-bold text-stone-900">Map Summary</h4>
<div className="absolute inset-x-4 bottom-4 z-10 rounded-2xl border border-white/20 bg-white/90 p-4 shadow-xl backdrop-blur-sm lg:inset-x-auto lg:bottom-8 lg:left-8 lg:max-w-xs">
<h4 className="mb-2 text-sm font-semibold text-stone-950">Map Summary</h4>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-stone-500">Total Leads on Map</span>
@@ -218,7 +215,7 @@ export function MapView({ user, jobIds }: MapViewProps) {
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-stone-500">Selected Lead</span>
<span className="max-w-[120px] truncate font-bold text-stone-900">{selected ? selected.name : 'None'}</span>
<span className="max-w-[140px] truncate font-bold text-stone-900 lg:max-w-[120px]">{selected ? selected.name : 'None'}</span>
</div>
{selectedJobCount > 0 && (
<div className="mt-2 border-t border-stone-200 pt-2">