Spaces:
Sleeping
Sleeping
File size: 4,791 Bytes
52a0642 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | /**
* P2: New-user onboarding — pick 3–5 books to seed preferences.
* Shown when myCollection is empty and onboarding not completed.
*/
import React, { useState, useEffect } from "react";
import { getOnboardingBooks } from "../api";
const PLACEHOLDER_IMG = "/content/cover-not-found.jpg";
const MIN_SELECT = 3;
const MAX_SELECT = 5;
const OnboardingModal = ({ onComplete, onAddFavorite, onSkip }) => {
const [books, setBooks] = useState([]);
const [selected, setSelected] = useState(new Set());
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
getOnboardingBooks(24)
.then(setBooks)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, []);
const toggle = (isbn) => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(isbn)) {
next.delete(isbn);
} else if (next.size < MAX_SELECT) {
next.add(isbn);
}
return next;
});
};
const handleComplete = async () => {
if (selected.size < MIN_SELECT) return;
try {
for (const isbn of selected) {
await onAddFavorite(isbn);
}
localStorage.setItem("onboarding_complete", "true");
onComplete();
} catch (e) {
setError(e.message);
}
};
const canComplete = selected.size >= MIN_SELECT;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="bg-white max-w-3xl w-full max-h-[90vh] overflow-hidden shadow-xl">
<div className="p-6 border-b border-[#eee]">
<h2 className="text-xl font-bold text-[#333]">Welcome — Pick Your Favorites</h2>
<p className="text-sm text-gray-500 mt-1">
Select 3–5 books you like to get personalized recommendations.
</p>
</div>
<div className="p-6 overflow-y-auto max-h-[50vh]">
{loading && (
<div className="text-center text-gray-400 py-8">Loading popular books...</div>
)}
{error && (
<div className="text-center text-red-500 py-4 text-sm">{error}</div>
)}
{!loading && !error && (
<div className="grid grid-cols-3 md:grid-cols-4 gap-4">
{books.map((book) => {
const isSelected = selected.has(book.isbn);
return (
<button
key={book.isbn}
type="button"
onClick={() => toggle(book.isbn)}
className={`text-left border-2 transition-all p-2 ${
isSelected ? "border-[#b392ac] bg-[#faf5f7]" : "border-[#eee] hover:border-[#ddd]"
}`}
>
<div className="aspect-[3/4] bg-gray-100 mb-2 overflow-hidden">
<img
src={book.thumbnail || PLACEHOLDER_IMG}
alt={book.title}
className="w-full h-full object-cover"
onError={(e) => {
e.target.onerror = null;
e.target.src = PLACEHOLDER_IMG;
}}
/>
</div>
<p className="text-[10px] font-bold text-[#555] truncate" title={book.title}>
{book.title}
</p>
{isSelected && (
<span className="text-[10px] text-[#b392ac] font-bold">✓ Selected</span>
)}
</button>
);
})}
</div>
)}
</div>
<div className="p-6 border-t border-[#eee] flex justify-between items-center">
<span className="text-xs text-gray-500">
{selected.size} selected (min {MIN_SELECT}, max {MAX_SELECT})
</span>
<div className="flex gap-2">
{onSkip && (
<button
type="button"
onClick={() => {
localStorage.setItem("onboarding_complete", "true");
onSkip();
}}
className="px-4 py-2 text-sm text-gray-500 hover:text-gray-700"
>
Skip for now
</button>
)}
<button
onClick={handleComplete}
disabled={!canComplete}
className={`px-6 py-2 text-sm font-bold ${
canComplete ? "bg-[#b392ac] text-white" : "bg-gray-200 text-gray-400 cursor-not-allowed"
}`}
>
Start Exploring
</button>
</div>
</div>
</div>
</div>
);
};
export default OnboardingModal;
|