feat(mobile): native-feel sticky top bar + bottom tab nav
Build & Push Docker Images / test-backend (push) Successful in 22s
Build & Push Docker Images / test-frontend (push) Failing after 23s
Build & Push Docker Images / build-backend (push) Has been skipped
Build & Push Docker Images / build-frontend (push) Has been skipped
Build & Push Docker Images / test-backend (push) Successful in 22s
Build & Push Docker Images / test-frontend (push) Failing after 23s
Build & Push Docker Images / build-backend (push) Has been skipped
Build & Push Docker Images / build-frontend (push) Has been skipped
Replaces the floating pill nav with persistent mobile chrome that
frames scrolling content the way iOS/Android native apps do.
New components:
- TopBar: brand left, contextual action right (Admin pill for admins,
bell for saved, Login button when logged out)
- BottomNav: 4-tab fixed nav (Search, Scan [elevated center FAB],
Saved, Profile). Saved/Profile disabled when logged out, tapping
opens LoginModal
- icons.jsx: 5 stroke-based SVG icons (Search, Scan, Bell, User, Cog)
plus Logo; replaces emoji in nav chrome
Behavior changes:
- App.jsx: tracks publicScreen ('home'|'scan'|'search') so BottomNav
can drive both top-level view + inner screen
- PublicView: screen state lifted to App via props (back-compat
defaults preserved so existing call sites don't break)
- App.css: old mobile .view-switcher block hidden (replaced by
BottomNav); .app padding bumped to clear the fixed bottom bar
Desktop (>768px) unchanged — existing centered pill nav still works,
new components hidden via display:none.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+2
-20
@@ -290,7 +290,7 @@
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.app {
|
.app {
|
||||||
padding: 1.5rem 1rem calc(5rem + env(safe-area-inset-bottom));
|
padding: 1rem 1rem calc(7rem + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header h1 {
|
.app-header h1 {
|
||||||
@@ -310,25 +310,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.view-switcher {
|
.view-switcher {
|
||||||
position: fixed;
|
display: none; /* ponytail: replaced by BottomNav on mobile */
|
||||||
left: 0.75rem;
|
|
||||||
right: 0.75rem;
|
|
||||||
bottom: calc(0.75rem + env(safe-area-inset-bottom));
|
|
||||||
z-index: 100;
|
|
||||||
width: auto;
|
|
||||||
max-width: 32rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
box-shadow:
|
|
||||||
0 10px 30px rgba(28, 25, 23, 0.12),
|
|
||||||
0 2px 6px rgba(28, 25, 23, 0.06);
|
|
||||||
padding: 0.5rem;
|
|
||||||
overflow: visible;
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-switcher button {
|
.view-switcher button {
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import AdminView from './views/AdminView';
|
|||||||
import ProfileView from './views/ProfileView';
|
import ProfileView from './views/ProfileView';
|
||||||
import LoginModal from './components/LoginModal';
|
import LoginModal from './components/LoginModal';
|
||||||
import SavedNotifications from './components/SavedNotifications';
|
import SavedNotifications from './components/SavedNotifications';
|
||||||
|
import TopBar from './components/TopBar';
|
||||||
|
import BottomNav from './components/BottomNav';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [view, setView] = useState('public');
|
const [view, setView] = useState('public');
|
||||||
|
const [publicScreen, setPublicScreen] = useState('home'); // 'home' | 'scan' | 'search'
|
||||||
const [currentUser, setCurrentUser] = useState(null);
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
const [authChecked, setAuthChecked] = useState(false);
|
const [authChecked, setAuthChecked] = useState(false);
|
||||||
const [showLogin, setShowLogin] = useState(false);
|
const [showLogin, setShowLogin] = useState(false);
|
||||||
@@ -30,6 +33,7 @@ function App() {
|
|||||||
await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
|
await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setView('public');
|
setView('public');
|
||||||
|
setPublicScreen('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProfileSaved(updated) {
|
function handleProfileSaved(updated) {
|
||||||
@@ -55,12 +59,47 @@ function App() {
|
|||||||
<PublicView
|
<PublicView
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLoginRequest={() => setShowLogin(true)}
|
onLoginRequest={() => setShowLogin(true)}
|
||||||
|
screen={publicScreen}
|
||||||
|
onScreenChange={setPublicScreen}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ponytail: BottomNav active tab derived from view + publicScreen. Hidden on desktop via CSS.
|
||||||
|
function navActiveTab() {
|
||||||
|
if (view === 'profile') return 'profile';
|
||||||
|
if (view === 'admin') return 'profile'; // no admin tab in bottom nav; profile still highlighted
|
||||||
|
if (publicScreen === 'scan') return 'scan';
|
||||||
|
if (publicScreen === 'search') return 'search';
|
||||||
|
return 'search'; // home → search is the default landing active
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavChange(tab) {
|
||||||
|
if (tab === 'search') { setView('public'); setPublicScreen('search'); return; }
|
||||||
|
if (tab === 'scan') { setView('public'); setPublicScreen('scan'); return; }
|
||||||
|
if (tab === 'saved') {
|
||||||
|
if (currentUser) { setShowSaved(true); setView('public'); setPublicScreen('home'); }
|
||||||
|
else setShowLogin(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tab === 'profile') {
|
||||||
|
if (currentUser) setView('profile');
|
||||||
|
else setShowLogin(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
|
<TopBar
|
||||||
|
currentUser={currentUser}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
authChecked={authChecked}
|
||||||
|
onShowSaved={() => setShowSaved(true)}
|
||||||
|
onLoginRequest={() => setShowLogin(true)}
|
||||||
|
onAdminClick={() => setView(view === 'admin' ? 'public' : 'admin')}
|
||||||
|
/>
|
||||||
|
|
||||||
<nav className="view-switcher" aria-label="Primary">
|
<nav className="view-switcher" aria-label="Primary">
|
||||||
<button
|
<button
|
||||||
className={view === 'public' ? 'active' : ''}
|
className={view === 'public' ? 'active' : ''}
|
||||||
@@ -97,6 +136,12 @@ function App() {
|
|||||||
|
|
||||||
{activeView}
|
{activeView}
|
||||||
|
|
||||||
|
<BottomNav
|
||||||
|
activeTab={navActiveTab()}
|
||||||
|
onChange={handleNavChange}
|
||||||
|
isLoggedIn={Boolean(currentUser)}
|
||||||
|
/>
|
||||||
|
|
||||||
{showLogin && (
|
{showLogin && (
|
||||||
<LoginModal
|
<LoginModal
|
||||||
onLogin={handleLogin}
|
onLogin={handleLogin}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/* BottomNav — mobile-only fixed bottom tab bar. Desktop hides via display:none above 768px. */
|
||||||
|
|
||||||
|
.bottom-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bottom-nav {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 50;
|
||||||
|
padding: 0.4rem 0.5rem calc(0.4rem + env(safe-area-inset-bottom));
|
||||||
|
background: var(--glass-bg);
|
||||||
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||||
|
backdrop-filter: saturate(180%) blur(20px);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem 0.25rem 0.1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font: inherit;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: inherit;
|
||||||
|
transition: background 0.2s, color 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elevated center tab (Scan) — sits above the bar like iOS center FAB */
|
||||||
|
.bottom-nav-item:nth-child(2) {
|
||||||
|
margin-top: -1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon-wrap--elevated {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
width: 3.25rem;
|
||||||
|
height: 3.25rem;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 4px var(--glass-bg),
|
||||||
|
0 8px 18px rgba(15, 118, 110, 0.4),
|
||||||
|
0 2px 6px rgba(15, 118, 110, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item.active .nav-label {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item.active .nav-icon-wrap:not(.nav-icon-wrap--elevated) {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav-item:active .nav-icon-wrap--elevated {
|
||||||
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { IconSearch, IconScan, IconBell, IconUser } from './icons';
|
||||||
|
import './BottomNav.css';
|
||||||
|
|
||||||
|
// ponytail: 4 tabs. Scan is the 2nd item (center) and gets the elevated teal pill.
|
||||||
|
// ponytail: when logged out, "saved" slot morphs into Login. Profile is disabled until login.
|
||||||
|
function BottomNav({ activeTab, onChange, isLoggedIn }) {
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'search', label: 'Search', Icon: IconSearch },
|
||||||
|
{ id: 'scan', label: 'Scan', Icon: IconScan, elevated: true },
|
||||||
|
{ id: 'saved', label: 'Saved', Icon: IconBell, requiresAuth: true },
|
||||||
|
{ id: 'profile', label: 'Profile', Icon: IconUser, requiresAuth: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="bottom-nav" aria-label="Primary">
|
||||||
|
{tabs.map(({ id, label, Icon, elevated, requiresAuth }) => {
|
||||||
|
const disabled = requiresAuth && !isLoggedIn;
|
||||||
|
const isActive = activeTab === id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
className={[
|
||||||
|
'bottom-nav-item',
|
||||||
|
isActive ? 'active' : '',
|
||||||
|
disabled ? 'disabled' : '',
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
onClick={() => onChange(id)}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
<span className={`nav-icon-wrap${elevated ? ' nav-icon-wrap--elevated' : ''}`}>
|
||||||
|
<Icon size={elevated ? 24 : 22} />
|
||||||
|
</span>
|
||||||
|
<span className="nav-label">{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BottomNav;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* TopBar — mobile-only sticky header. Desktop hides via display:none above 768px. */
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
padding: calc(0.65rem + env(safe-area-inset-top)) 1rem 0.65rem;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||||
|
backdrop-filter: saturate(180%) blur(20px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-brand-icon {
|
||||||
|
color: var(--primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-brand-text {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-main);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-action:hover {
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-action--admin {
|
||||||
|
color: var(--primary);
|
||||||
|
border-color: rgba(15, 118, 110, 0.25);
|
||||||
|
background: rgba(15, 118, 110, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-action--admin:hover {
|
||||||
|
background: rgba(15, 118, 110, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-action--login {
|
||||||
|
color: var(--primary);
|
||||||
|
border-color: rgba(15, 118, 110, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-action--placeholder {
|
||||||
|
width: 2.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { IconBell, IconCog, IconUser, IconLogo } from './icons';
|
||||||
|
import './TopBar.css';
|
||||||
|
|
||||||
|
function TopBar({
|
||||||
|
currentUser,
|
||||||
|
isAdmin,
|
||||||
|
onShowSaved,
|
||||||
|
onLoginRequest,
|
||||||
|
onAdminClick,
|
||||||
|
authChecked,
|
||||||
|
}) {
|
||||||
|
// ponytail: action slot priority — admin pill > bell (logged-in) > login button (logged-out).
|
||||||
|
// ponytail: hidden on desktop via .top-bar { display: none } above 768px.
|
||||||
|
function renderAction() {
|
||||||
|
if (isAdmin) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="topbar-action topbar-action--admin"
|
||||||
|
onClick={onAdminClick}
|
||||||
|
aria-label="Admin panel"
|
||||||
|
>
|
||||||
|
<IconCog size={18} />
|
||||||
|
<span>Admin</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentUser) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="topbar-action"
|
||||||
|
onClick={onShowSaved}
|
||||||
|
aria-label="Saved notifications"
|
||||||
|
>
|
||||||
|
<IconBell size={22} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (authChecked) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="topbar-action topbar-action--login"
|
||||||
|
onClick={onLoginRequest}
|
||||||
|
>
|
||||||
|
<IconUser size={18} />
|
||||||
|
<span>Login</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span className="topbar-action topbar-action--placeholder" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="top-bar">
|
||||||
|
<div className="topbar-brand">
|
||||||
|
<IconLogo size={22} className="topbar-brand-icon" />
|
||||||
|
<span className="topbar-brand-text">FarmaFinder</span>
|
||||||
|
</div>
|
||||||
|
<div className="topbar-actions">{renderAction()}</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopBar;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// ponytail: stroke-based line icons, 24x24, currentColor. Emojis stay for in-content use.
|
||||||
|
// ponytail: add when a filled variant is needed for active states — swap stroke="currentColor" for fill.
|
||||||
|
|
||||||
|
export function IconSearch({ size = 24, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
|
||||||
|
<circle cx="11" cy="11" r="7" />
|
||||||
|
<path d="m20 20-3.5-3.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconScan({ size = 24, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
|
||||||
|
<path d="M3 7V5a2 2 0 0 1 2-2h2" />
|
||||||
|
<path d="M17 3h2a2 2 0 0 1 2 2v2" />
|
||||||
|
<path d="M21 17v2a2 2 0 0 1-2 2h-2" />
|
||||||
|
<path d="M7 21H5a2 2 0 0 1-2-2v-2" />
|
||||||
|
<path d="M7 8v8M11 8v8M15 8v8M19 8v8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconBell({ size = 24, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
|
||||||
|
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
||||||
|
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconUser({ size = 24, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconCog({ size = 24, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconLogo({ size = 24, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
|
||||||
|
<path d="M19 8h-1V6a3 3 0 0 0-3-3H9a3 3 0 0 0-3 3v2H5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-6a3 3 0 0 0-3-3zM8 6a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2H8V6zm4 11a3 3 0 1 1 3-3 3 3 0 0 1-3 3z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,9 +8,15 @@ import HomeView from './HomeView';
|
|||||||
import ScannerView from './ScannerView';
|
import ScannerView from './ScannerView';
|
||||||
import { haversineKm, getUserPosition } from '../utils/geo';
|
import { haversineKm, getUserPosition } from '../utils/geo';
|
||||||
|
|
||||||
function PublicView({ currentUser, onLoginRequest }) {
|
function PublicView({
|
||||||
// 'home' | 'scan' | 'search'
|
currentUser,
|
||||||
const [screen, setScreen] = useState('home');
|
onLoginRequest,
|
||||||
|
screen: screenProp,
|
||||||
|
onScreenChange,
|
||||||
|
}) {
|
||||||
|
// ponytail: screen state lifted to App.jsx so BottomNav can switch directly.
|
||||||
|
const screen = screenProp ?? 'home';
|
||||||
|
const setScreen = onScreenChange ?? (() => {});
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [medicines, setMedicines] = useState([]);
|
const [medicines, setMedicines] = useState([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user