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:
+18
-21
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user