fix: onboard users without households
This commit is contained in:
parent
dc422f6127
commit
5510401635
@ -1,15 +1,47 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { HouseholdContext } from '../../context/HouseholdContext';
|
||||
import '../../styles/components/HouseholdSwitcher.css';
|
||||
import CreateJoinHousehold from '../manage/CreateJoinHousehold';
|
||||
import { useContext, useState } from "react";
|
||||
import { HouseholdContext } from "../../context/HouseholdContext";
|
||||
import "../../styles/components/HouseholdSwitcher.css";
|
||||
import CreateJoinHousehold from "../manage/CreateJoinHousehold";
|
||||
|
||||
export default function HouseholdSwitcher() {
|
||||
const { households, activeHousehold, setActiveHousehold, loading } = useContext(HouseholdContext);
|
||||
const {
|
||||
households,
|
||||
activeHousehold,
|
||||
setActiveHousehold,
|
||||
loading,
|
||||
hasLoaded,
|
||||
} = useContext(HouseholdContext);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showCreateJoin, setShowCreateJoin] = useState(false);
|
||||
|
||||
if (!hasLoaded || loading || (households.length > 0 && !activeHousehold)) {
|
||||
return (
|
||||
<div className="household-switcher household-switcher-empty">
|
||||
<button className="household-switcher-toggle" type="button" disabled>
|
||||
<span className="household-name">Loading households...</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeHousehold || households.length === 0) {
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
<div className="household-switcher household-switcher-empty">
|
||||
<button
|
||||
className="household-switcher-toggle household-switcher-cta"
|
||||
type="button"
|
||||
onClick={() => setShowCreateJoin(true)}
|
||||
>
|
||||
<span className="household-name">Create or Join Household</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showCreateJoin && (
|
||||
<CreateJoinHousehold onClose={() => setShowCreateJoin(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSelect = (household) => {
|
||||
@ -21,32 +53,35 @@ export default function HouseholdSwitcher() {
|
||||
<div className="household-switcher">
|
||||
<button
|
||||
className="household-switcher-toggle"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
type="button"
|
||||
onClick={() => setIsOpen((current) => !current)}
|
||||
disabled={loading}
|
||||
>
|
||||
<span className="household-name">{activeHousehold.name}</span>
|
||||
<span className={`dropdown-icon ${isOpen ? 'open' : ''}`}>▼</span>
|
||||
<span className={`dropdown-icon ${isOpen ? "open" : ""}`}>▾</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className="household-switcher-overlay" onClick={() => setIsOpen(false)} />
|
||||
<div className="household-switcher-dropdown">
|
||||
{households.map(household => (
|
||||
{households.map((household) => (
|
||||
<button
|
||||
key={household.id}
|
||||
className={`household-option ${household.id === activeHousehold.id ? 'active' : ''}`}
|
||||
className={`household-option ${household.id === activeHousehold.id ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => handleSelect(household)}
|
||||
>
|
||||
{household.name}
|
||||
{household.id === activeHousehold.id && (
|
||||
<span className="check-mark">✓</span>
|
||||
<span className="check-mark">✓</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div className="household-divider"></div>
|
||||
<button
|
||||
className="household-option create-household-btn"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setShowCreateJoin(true);
|
||||
|
||||
69
frontend/src/components/household/NoHouseholdState.jsx
Normal file
69
frontend/src/components/household/NoHouseholdState.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { HouseholdContext } from "../../context/HouseholdContext";
|
||||
import "../../styles/components/NoHouseholdState.css";
|
||||
import CreateJoinHousehold from "../manage/CreateJoinHousehold";
|
||||
|
||||
export default function NoHouseholdState({
|
||||
title = "No household yet",
|
||||
description = "Create a household to start building lists, or join one with an invite code or invite link.",
|
||||
}) {
|
||||
const { error, refreshHouseholds } = useContext(HouseholdContext);
|
||||
const [showCreateJoin, setShowCreateJoin] = useState(false);
|
||||
const [modalMode, setModalMode] = useState("create");
|
||||
|
||||
const openModal = (nextMode) => {
|
||||
setModalMode(nextMode);
|
||||
setShowCreateJoin(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="no-household-state" aria-live="polite">
|
||||
<div className="no-household-card">
|
||||
<p className="no-household-eyebrow">Welcome</p>
|
||||
<h2 className="no-household-title">{title}</h2>
|
||||
<p className="no-household-description">{description}</p>
|
||||
|
||||
{error && (
|
||||
<p className="no-household-error">
|
||||
We couldn't load households: {error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="no-household-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary no-household-action"
|
||||
onClick={() => openModal("create")}
|
||||
>
|
||||
Create Household
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary no-household-action"
|
||||
onClick={() => openModal("join")}
|
||||
>
|
||||
Join Household
|
||||
</button>
|
||||
{error && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary no-household-action"
|
||||
onClick={refreshHouseholds}
|
||||
>
|
||||
Retry Loading
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showCreateJoin && (
|
||||
<CreateJoinHousehold
|
||||
initialMode={modalMode}
|
||||
onClose={() => setShowCreateJoin(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,21 +1,26 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createHousehold, joinHousehold } from "../../api/households";
|
||||
import { joinHousehold } from "../../api/households";
|
||||
import { HouseholdContext } from "../../context/HouseholdContext";
|
||||
import useActionToast from "../../hooks/useActionToast";
|
||||
import getApiErrorMessage from "../../lib/getApiErrorMessage";
|
||||
import "../../styles/components/manage/CreateJoinHousehold.css";
|
||||
|
||||
export default function CreateJoinHousehold({ onClose }) {
|
||||
export default function CreateJoinHousehold({ initialMode = "create", onClose }) {
|
||||
const navigate = useNavigate();
|
||||
const toast = useActionToast();
|
||||
const { refreshHouseholds } = useContext(HouseholdContext);
|
||||
const [mode, setMode] = useState("create");
|
||||
const { createHousehold: createHouseholdWithContext, refreshHouseholds } = useContext(HouseholdContext);
|
||||
const [mode, setMode] = useState(initialMode === "join" ? "join" : "create");
|
||||
const [householdName, setHouseholdName] = useState("");
|
||||
const [inviteCode, setInviteCode] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setMode(initialMode === "join" ? "join" : "create");
|
||||
setError("");
|
||||
}, [initialMode]);
|
||||
|
||||
const extractInviteToken = (value) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
@ -27,7 +32,7 @@ export default function CreateJoinHousehold({ onClose }) {
|
||||
const parsed = new URL(trimmed, window.location.origin);
|
||||
const urlMatch = parsed.pathname.match(/^\/invite\/([a-zA-Z0-9]+)$/);
|
||||
if (urlMatch) return urlMatch[1];
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -42,8 +47,7 @@ export default function CreateJoinHousehold({ onClose }) {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
await createHousehold(householdName);
|
||||
await refreshHouseholds();
|
||||
await createHouseholdWithContext(householdName);
|
||||
toast.success("Created household", `Created household ${householdName.trim()}`);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
@ -91,7 +95,14 @@ export default function CreateJoinHousehold({ onClose }) {
|
||||
<div className="create-join-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>Household</h2>
|
||||
<button className="close-btn" onClick={onClose}>×</button>
|
||||
<button
|
||||
className="close-btn"
|
||||
type="button"
|
||||
aria-label="Close household dialog"
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mode-tabs">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { createHousehold as createHouseholdApi, getUserHouseholds } from '../api/households';
|
||||
import { AuthContext } from './AuthContext';
|
||||
|
||||
@ -6,6 +6,7 @@ export const HouseholdContext = createContext({
|
||||
households: [],
|
||||
activeHousehold: null,
|
||||
loading: false,
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
setActiveHousehold: () => { },
|
||||
refreshHouseholds: () => { },
|
||||
@ -17,27 +18,65 @@ export const HouseholdProvider = ({ children }) => {
|
||||
const [households, setHouseholds] = useState([]);
|
||||
const [activeHousehold, setActiveHouseholdState] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const clearActiveHousehold = useCallback(() => {
|
||||
setActiveHouseholdState(null);
|
||||
localStorage.removeItem('activeHouseholdId');
|
||||
}, []);
|
||||
|
||||
const loadHouseholds = useCallback(async () => {
|
||||
if (!token) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
console.log('[HouseholdContext] Loading households...');
|
||||
const response = await getUserHouseholds();
|
||||
const nextHouseholds = Array.isArray(response.data) ? response.data : [];
|
||||
console.log('[HouseholdContext] Loaded households:', nextHouseholds);
|
||||
setHouseholds(nextHouseholds);
|
||||
|
||||
if (nextHouseholds.length === 0) {
|
||||
clearActiveHousehold();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[HouseholdContext] Failed to load households:', err);
|
||||
setError(err.response?.data?.message || 'Failed to load households');
|
||||
setHouseholds([]);
|
||||
clearActiveHousehold();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setHasLoaded(true);
|
||||
}
|
||||
}, [clearActiveHousehold, token]);
|
||||
|
||||
// Load households on mount and when token changes
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
setHasLoaded(false);
|
||||
loadHouseholds();
|
||||
} else {
|
||||
// Clear state when logged out
|
||||
setHouseholds([]);
|
||||
setActiveHouseholdState(null);
|
||||
clearActiveHousehold();
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
setHasLoaded(false);
|
||||
}
|
||||
}, [token]);
|
||||
}, [clearActiveHousehold, loadHouseholds, token]);
|
||||
|
||||
// Load active household from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (households.length === 0) return;
|
||||
if (households.length === 0) {
|
||||
clearActiveHousehold();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HouseholdContext] Setting active household from:', households);
|
||||
const savedHouseholdId = localStorage.getItem('activeHouseholdId');
|
||||
if (savedHouseholdId) {
|
||||
const household = households.find(h => h.id === parseInt(savedHouseholdId));
|
||||
const household = households.find(h => h.id === parseInt(savedHouseholdId, 10));
|
||||
if (household) {
|
||||
console.log('[HouseholdContext] Found saved household:', household);
|
||||
setActiveHouseholdState(household);
|
||||
@ -49,26 +88,7 @@ export const HouseholdProvider = ({ children }) => {
|
||||
console.log('[HouseholdContext] Using first household:', households[0]);
|
||||
setActiveHouseholdState(households[0]);
|
||||
localStorage.setItem('activeHouseholdId', households[0].id);
|
||||
}, [households]);
|
||||
|
||||
const loadHouseholds = async () => {
|
||||
if (!token) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
console.log('[HouseholdContext] Loading households...');
|
||||
const response = await getUserHouseholds();
|
||||
console.log('[HouseholdContext] Loaded households:', response.data);
|
||||
setHouseholds(response.data);
|
||||
} catch (err) {
|
||||
console.error('[HouseholdContext] Failed to load households:', err);
|
||||
setError(err.response?.data?.message || 'Failed to load households');
|
||||
setHouseholds([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [clearActiveHousehold, households]);
|
||||
|
||||
const setActiveHousehold = (household) => {
|
||||
setActiveHouseholdState(household);
|
||||
@ -101,6 +121,7 @@ export const HouseholdProvider = ({ children }) => {
|
||||
households,
|
||||
activeHousehold,
|
||||
loading,
|
||||
hasLoaded,
|
||||
error,
|
||||
setActiveHousehold,
|
||||
refreshHouseholds: loadHouseholds,
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { getHouseholdMembers } from "../api/households";
|
||||
import SortDropdown from "../components/common/SortDropdown";
|
||||
import AddItemForm from "../components/forms/AddItemForm";
|
||||
import NoHouseholdState from "../components/household/NoHouseholdState";
|
||||
import GroceryListItem from "../components/items/GroceryListItem";
|
||||
import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal";
|
||||
import ConfirmBuyModal from "../components/modals/ConfirmBuyModal";
|
||||
@ -80,7 +81,12 @@ function getNextModalItem(sortedItems, currentIndex, excludedItemId) {
|
||||
export default function GroceryList() {
|
||||
const pageTitle = "Grocery List";
|
||||
const { userId } = useContext(AuthContext);
|
||||
const { activeHousehold } = useContext(HouseholdContext);
|
||||
const {
|
||||
activeHousehold,
|
||||
households,
|
||||
loading: householdLoading,
|
||||
hasLoaded: householdsLoaded
|
||||
} = useContext(HouseholdContext);
|
||||
const { activeStore, stores, loading: storeLoading } = useContext(StoreContext);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const toast = useActionToast();
|
||||
@ -762,14 +768,25 @@ export default function GroceryList() {
|
||||
};
|
||||
|
||||
|
||||
if (!householdsLoaded || householdLoading || (households.length > 0 && !activeHousehold)) {
|
||||
return (
|
||||
<div className="glist-body">
|
||||
<div className="glist-container">
|
||||
<h1 className="glist-title">{pageTitle}</h1>
|
||||
<p style={{ textAlign: "center", marginTop: "2rem", color: "var(--text-secondary)" }}>
|
||||
Loading households...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeHousehold) {
|
||||
return (
|
||||
<div className="glist-body">
|
||||
<div className="glist-container">
|
||||
<h1 className="glist-title">{pageTitle}</h1>
|
||||
<p style={{ textAlign: 'center', marginTop: '2rem', color: 'var(--text-secondary)' }}>
|
||||
Loading households...
|
||||
</p>
|
||||
<NoHouseholdState />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import NoHouseholdState from "../components/household/NoHouseholdState";
|
||||
import ManageHousehold from "../components/manage/ManageHousehold";
|
||||
import ManageStores from "../components/manage/ManageStores";
|
||||
import { HouseholdContext } from "../context/HouseholdContext";
|
||||
import "../styles/pages/Manage.css";
|
||||
|
||||
export default function Manage() {
|
||||
const { activeHousehold } = useContext(HouseholdContext);
|
||||
const { activeHousehold, households, loading, hasLoaded } = useContext(HouseholdContext);
|
||||
const [activeTab, setActiveTab] = useState("household");
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@ -17,14 +18,25 @@ export default function Manage() {
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
if (!hasLoaded || loading || (households.length > 0 && !activeHousehold)) {
|
||||
return (
|
||||
<div className="manage-body">
|
||||
<div className="manage-container">
|
||||
<h1 className="manage-title">Manage</h1>
|
||||
<p style={{ textAlign: "center", marginTop: "2rem", color: "var(--text-secondary)" }}>
|
||||
Loading household...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeHousehold) {
|
||||
return (
|
||||
<div className="manage-body">
|
||||
<div className="manage-container">
|
||||
<h1 className="manage-title">Manage</h1>
|
||||
<p style={{ textAlign: 'center', marginTop: '2rem', color: 'var(--text-secondary)' }}>
|
||||
Loading household...
|
||||
</p>
|
||||
<NoHouseholdState />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.household-switcher {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.household-switcher-toggle {
|
||||
@ -8,6 +9,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
@ -16,7 +18,6 @@
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.household-switcher-toggle:hover {
|
||||
@ -29,20 +30,30 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.household-switcher-cta {
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.household-switcher-empty .household-switcher-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.household-name {
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dropdown-icon.open {
|
||||
@ -51,10 +62,7 @@
|
||||
|
||||
.household-switcher-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
@ -64,12 +72,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--card-bg);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.household-option {
|
||||
@ -98,19 +106,21 @@
|
||||
}
|
||||
|
||||
.household-option.active {
|
||||
background: rgba(30, 144, 255, 0.15);
|
||||
background: color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.household-divider {);
|
||||
.household-divider {
|
||||
height: 1px;
|
||||
margin: 0.25rem 0;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.create-household-btn {
|
||||
@ -119,7 +129,11 @@
|
||||
}
|
||||
|
||||
.create-household-btn:hover {
|
||||
background: rgba(30, 144, 255, 0.15
|
||||
.create-household-btn:hover {
|
||||
background: var(--primary-color-light);
|
||||
background: color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.household-switcher {
|
||||
min-width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
76
frontend/src/styles/components/NoHouseholdState.css
Normal file
76
frontend/src/styles/components/NoHouseholdState.css
Normal file
@ -0,0 +1,76 @@
|
||||
.no-household-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 2rem auto 0;
|
||||
}
|
||||
|
||||
.no-household-card {
|
||||
width: min(100%, 38rem);
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
radial-gradient(circle at top right, color-mix(in srgb, var(--primary) 14%, transparent), transparent 38%),
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 96%, white), var(--card-bg));
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.12);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-household-eyebrow {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.no-household-title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.75rem, 4vw, 2.4rem);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.no-household-description {
|
||||
margin: 1rem 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.no-household-error {
|
||||
margin: 1rem 0 0;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 12px;
|
||||
background: var(--danger-light, rgba(220, 53, 69, 0.1));
|
||||
color: var(--danger);
|
||||
border: 1px solid color-mix(in srgb, var(--danger) 50%, transparent);
|
||||
}
|
||||
|
||||
.no-household-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.no-household-action {
|
||||
min-width: 11rem;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.no-household-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.no-household-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.no-household-action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
59
frontend/tests/household-onboarding.spec.ts
Normal file
59
frontend/tests/household-onboarding.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
function seedAuthStorage(page: import("@playwright/test").Page) {
|
||||
return page.addInitScript(() => {
|
||||
localStorage.setItem("token", "test-token");
|
||||
localStorage.setItem("userId", "1");
|
||||
localStorage.setItem("role", "admin");
|
||||
localStorage.setItem("username", "new-user");
|
||||
});
|
||||
}
|
||||
|
||||
async function mockConfig(page: import("@playwright/test").Page) {
|
||||
await page.route("**/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
maxFileSizeMB: 20,
|
||||
maxImageDimension: 800,
|
||||
imageQuality: 85,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("new users with no households see create and join actions instead of a loading dead-end", async ({ page }) => {
|
||||
await seedAuthStorage(page);
|
||||
await mockConfig(page);
|
||||
|
||||
await page.route("**/households", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByRole("button", { name: "Create or Join Household" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "No household yet" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Create Household" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Join Household" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Join Household" }).click();
|
||||
await expect(page.getByLabel("Invite Code or Link")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close household dialog" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Create Household" }).click();
|
||||
await expect(page.getByLabel("Household Name")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close household dialog" }).click();
|
||||
|
||||
await page.goto("/manage");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Manage" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "No household yet" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Create Household" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Join Household" })).toBeVisible();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user