- {navItems.map((item) => {
- const isActive = activeView === item.view;
- const Icon = item.icon;
+
+ {/* Container is invisible, items are scattered */}
+
+ {navItems.map((item) => (
+
!item.disabled && onNavigate(item.id)}
+ disabled={item.disabled}
+ className={`
+ relative group flex flex-col items-center justify-center
+ w-16 h-20 sm:w-20 sm:h-24
+ bg-white p-2 pb-6 sm:pb-8
+ shadow-scrap-card transition-all duration-300 ease-out
+ border border-gray-100
+ ${item.disabled ? "opacity-50 grayscale cursor-not-allowed" : "hover:z-50 hover:scale-110 hover:-translate-y-2 hover:shadow-scrap-lift cursor-pointer"}
+ `}
+ style={{
+ transform: `rotate(${item.rotation}deg)`,
+ transitionDelay: `${item.delay}s`
+ }}
+ >
+ {/* Polaroid Photo Area */}
+
+
+
- return (
- !item.disabled && onNavigate(item.view)}
- disabled={item.disabled}
- className={`h-11 px-3 rounded-xl flex items-center gap-2 text-sm transition-colors ${isActive
- ? "bg-light-accent/12 dark:bg-dark-accent/18 text-light-accent dark:text-dark-accent"
- : item.disabled
- ? "text-light-text-muted/40 dark:text-dark-text-muted/40 cursor-not-allowed"
- : "text-light-text-muted dark:text-dark-text-muted hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
- }`}
- aria-label={item.label}
- aria-current={isActive ? "page" : undefined}
- >
-
- {item.label}
-
- );
- })}
+ {/* Handwritten Label */}
+
+ {item.label}
+
+
+ {/* Tape Strip (Top Center) - Only visible on active or hover */}
+
+
+ ))}
);
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
new file mode 100644
index 0000000..17be8f9
--- /dev/null
+++ b/apps/web/src/index.css
@@ -0,0 +1,186 @@
+@import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;1,400;1,600&family=Nunito:wght@400;600;700&family=VT323&display=swap');
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ /* Scrapbook Palette Variables (New) */
+ --scrap-cream: 253, 251, 247;
+ --scrap-navy: 44, 58, 79;
+ --scrap-sage: 139, 154, 139;
+ --scrap-blue: 91, 112, 138;
+ --scrap-kraft: 229, 217, 197;
+
+ /* Cozy Library Palette (Old - Kept for compatibility) */
+ --paper-cream: 253 251 247; /* #FDFBF7 */
+ --aged-paper: 245 230 211; /* #F5E6D3 */
+ --ink-navy: 28 42 58; /* #1C2A3A */
+ --sepia-brown: 62 39 35; /* #3E2723 */
+ --sage-green: 126 156 142; /* #7E9C8E */
+ --woodstock-gold: 255 213 79; /* #FFD54F */
+ --clay-red: 211 84 0; /* #D35400 */
+
+ /* Pixel Art Touches */
+ --pixel-border: 2px solid rgb(var(--ink-navy));
+
+ /* Global Background: Noise Texture */
+ --noise-svg: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.08'/%3E%3C/svg%3E");
+
+ /* Torn Paper Clip Paths */
+ --torn-edge-top: polygon(0% 10px, 5% 0px, 10% 12px, 15% 2px, 20% 10px, 25% 0px, 30% 12px, 35% 2px, 40% 10px, 45% 0px, 50% 12px, 55% 2px, 60% 10px, 65% 0px, 70% 12px, 75% 2px, 80% 10px, 85% 0px, 90% 12px, 95% 2px, 100% 10px, 100% 100%, 0% 100%);
+ --torn-edge-all: polygon(
+ 2% 0%, 5% 2%, 10% 0%, 15% 3%, 20% 0%, 25% 2%, 30% 0%, 35% 3%, 40% 0%, 45% 2%, 50% 0%, 55% 3%, 60% 0%, 65% 2%, 70% 0%, 75% 3%, 80% 0%, 85% 2%, 90% 0%, 95% 3%, 98% 0%,
+ 100% 2%, 98% 5%, 100% 10%, 98% 15%, 100% 20%, 98% 25%, 100% 30%, 98% 35%, 100% 40%, 98% 45%, 100% 50%, 98% 55%, 100% 60%, 98% 65%, 100% 70%, 98% 75%, 100% 80%, 98% 85%, 100% 90%, 98% 95%, 100% 98%,
+ 98% 100%, 95% 98%, 90% 100%, 85% 97%, 80% 100%, 75% 97%, 70% 100%, 65% 97%, 60% 100%, 55% 97%, 50% 100%, 45% 97%, 40% 100%, 35% 97%, 30% 100%, 25% 97%, 20% 100%, 15% 97%, 10% 100%, 5% 97%, 2% 100%,
+ 0% 98%, 2% 95%, 0% 90%, 2% 85%, 0% 80%, 2% 75%, 0% 70%, 2% 65%, 0% 60%, 2% 55%, 0% 50%, 2% 45%, 0% 40%, 2% 35%, 0% 30%, 2% 25%, 0% 20%, 2% 15%, 0% 10%, 2% 5%, 0% 2%
+ );
+ }
+
+ body {
+ background-color: rgb(var(--scrap-cream));
+ color: rgb(var(--scrap-navy));
+ @apply font-sans antialiased overflow-x-hidden;
+ }
+
+ /* Compatibility for old components */
+ h1, h2, h3, h4, h5, h6 {
+ @apply font-serif font-semibold;
+ }
+
+ /* Custom Scrollbar */
+ ::-webkit-scrollbar {
+ width: 12px;
+ background-color: rgb(var(--scrap-cream));
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: rgb(var(--scrap-kraft));
+ border: 3px solid rgb(var(--scrap-cream));
+ border-radius: 99px;
+ }
+ ::-webkit-scrollbar-thumb:hover {
+ background-color: rgb(var(--scrap-sage));
+ }
+}
+
+@layer components {
+ /* Cozy Primitives (Old) */
+ .card-paper {
+ @apply bg-white border border-[rgb(var(--aged-paper))] rounded-xl shadow-sm transition-all duration-300;
+ }
+
+ .btn-cozy {
+ @apply inline-flex items-center justify-center rounded-full px-4 py-2 font-medium transition-all duration-200 active:scale-95;
+ @apply bg-[rgb(var(--ink-navy))] text-[rgb(var(--paper-cream))] hover:bg-[rgb(var(--sage-green))];
+ }
+
+ .input-underline {
+ @apply bg-transparent border-b-2 border-[rgb(var(--aged-paper))] px-2 py-1 outline-none transition-colors focus:border-[rgb(var(--sage-green))];
+ }
+
+ /* Pixel Art (Old) */
+ .pixel-border {
+ border: 2px solid rgb(var(--ink-navy));
+ box-shadow: 4px 4px 0px rgb(var(--ink-navy));
+ }
+
+ .pixel-btn {
+ @apply font-pixel text-xs px-4 py-2 bg-[rgb(var(--woodstock-gold))] text-[rgb(var(--ink-navy))] uppercase tracking-widest border-2 border-[rgb(var(--ink-navy))] hover:translate-y-0.5 hover:shadow-none active:translate-y-1 transition-all;
+ box-shadow: 3px 3px 0px rgb(var(--ink-navy));
+ }
+}
+
+@layer utilities {
+ .font-head {
+ font-family: 'Fredoka', cursive;
+ }
+
+ .font-body {
+ font-family: 'Courier Prime', monospace;
+ }
+
+ .font-accent {
+ font-family: 'Playfair Display', serif;
+ }
+
+ /* Old fonts mapping */
+ .font-serif {
+ font-family: 'Crimson Pro', serif;
+ }
+ .font-pixel {
+ font-family: 'VT323', monospace;
+ }
+
+ .text-shadow-sm {
+ text-shadow: 1px 1px 0px rgba(44, 58, 79, 0.1);
+ }
+
+ .bg-noise {
+ background-image: var(--noise-svg);
+ }
+
+ .mask-torn-top {
+ clip-path: var(--torn-edge-top);
+ }
+
+ .mask-torn-all {
+ clip-path: var(--torn-edge-all);
+ }
+
+ /* Washi Tape Effect */
+ .washi-tape {
+ @apply absolute w-12 h-6 bg-scrap-sage/60 rotate-[-5deg] backdrop-blur-[1px] shadow-sm;
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,0.3) 5px, rgba(255,255,255,0.3) 10px);
+ box-shadow: 1px 1px 2px rgba(0,0,0,0.1);
+ z-index: 20;
+ }
+
+ .washi-tape-blue {
+ @apply bg-scrap-blue/60 rotate-[3deg];
+ }
+
+ .washi-tape-kraft {
+ @apply bg-scrap-kraft/80 rotate-[-2deg];
+ }
+
+ /* Polaroid/Card Styles */
+ .card-polaroid {
+ @apply bg-white p-2 pb-8 shadow-scrap-card transition-all duration-300 border border-black/5;
+ }
+
+ .card-polaroid:hover {
+ @apply shadow-scrap-lift -translate-y-1 rotate-1;
+ z-index: 30;
+ }
+
+ /* Stamp Style */
+ .stamp-edge {
+ --r: 3px; /* Radius of circles */
+ background: radial-gradient(var(--r), transparent 95%, #fff 100%) 0 0/calc(2*var(--r)) calc(2*var(--r)),
+ linear-gradient(#fff 0 0);
+ background-composite: destination-in; /* Standard */
+ mask: radial-gradient(var(--r), #0000 95%, #000 100%) 0 0/calc(2*var(--r)) calc(2*var(--r)),
+ linear-gradient(#000 0 0);
+ mask-composite: exclude;
+ }
+}
+
+/* Animations */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.animate-fadeIn {
+ animation: fadeIn 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
+}
+
+@keyframes spin-slow {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.animate-spin-slow {
+ animation: spin-slow 12s linear infinite;
+}
diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx
index f6d6019..1b6a454 100644
--- a/apps/web/src/index.tsx
+++ b/apps/web/src/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
+import './index.css';
import { SettingsProvider } from '@/context/SettingsContext';
import { ErrorBoundary } from './components/ui/ErrorBoundary';
import { AuthProvider } from '@/hooks/useAuth';
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index 0f3c8b5..146306f 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -1,132 +1,79 @@
/** @type {import('tailwindcss').Config} */
export default {
+ darkMode: 'class',
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
- darkMode: 'class',
theme: {
extend: {
- fontFamily: {
- sans: ['Nunito', 'system-ui', 'sans-serif'],
- serif: ['Crimson Pro', 'Georgia', 'serif'],
- mono: ['JetBrains Mono', 'Menlo', 'Monaco', 'monospace'],
- pixel: ['"VT323"', 'monospace'],
- },
colors: {
- // Light theme colors (Cozy Library)
+ // Updated Maximalist/Scrapbook Palette
+ scrap: {
+ cream: '#FDFBF7', // Pale, buttery cream
+ navy: '#2C3A4F', // Deep slate navy
+ sage: '#8B9A8B', // Muted sage green
+ blue: '#5B708A', // Faded dusty blue
+ kraft: '#E5D9C5', // Faded kraft paper
+ ink: '#1A2333', // Darker ink for emphasis (optional)
+ },
+ // Keeping semantic tokens for compatibility but remapping them
light: {
- primary: 'rgb(var(--paper-cream))',
- secondary: 'rgb(var(--aged-paper))',
- surface: '#ffffff',
- card: '#ffffff',
- accent: 'rgb(var(--sage-green))',
- text: 'rgb(var(--ink-navy))',
- 'text-muted': 'rgb(var(--sepia-brown))',
- border: 'rgb(var(--aged-paper))',
- 'border-muted': '#f0efea',
+ primary: '#FDFBF7', // scrap-cream
+ surface: '#FFFFFF', // pure white (for cards/torn paper)
+ accent: '#5B708A', // scrap-blue
+ text: '#2C3A4F', // scrap-navy
+ 'text-muted': '#5B708A', // scrap-blue
},
- // Dark theme colors (Night Reading) - kept similar structure but warmer
dark: {
- primary: '#1A1410',
- secondary: '#2C2319',
- surface: '#252320',
- card: '#2a2825',
- accent: '#D4AF37', // Gold
- text: '#E8DCC8', // Parchment
- 'text-muted': '#B8A890',
- border: '#3E2723',
- 'border-muted': '#2f2e2b',
- },
+ // Minimal Dark Mode Support (inverts logic slightly but keeps vibe)
+ primary: '#1A2333', // dark-ink
+ surface: '#2C3A4F', // scrap-navy
+ accent: '#8B9A8B', // scrap-sage
+ text: '#FDFBF7', // scrap-cream
+ 'text-muted': '#E5D9C5', // scrap-kraft
+ }
+ },
+ fontFamily: {
+ // Updated Fonts
+ 'scrap-head': ['Fredoka', 'Sniglet', 'cursive'],
+ 'scrap-body': ['"Courier Prime"', '"Special Elite"', 'monospace'],
+ 'scrap-accent': ['"Playfair Display"', 'serif'],
+ // Remapping existing font tokens
+ sans: ['"Courier Prime"', 'monospace'], // Default to typewriter look
+ serif: ['"Playfair Display"', 'serif'],
+ mono: ['"Courier Prime"', 'monospace'],
+ pixel: ['"Press Start 2P"', 'cursive'], // Keep for legacy pixel elements if any
+ hand: ['"Indie Flower"', 'cursive'], // Keep for legacy hand-drawn elements
+ },
+ backgroundImage: {
+ 'noise': "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.08'/%3E%3C/svg%3E\")",
},
boxShadow: {
- 'pixel': '4px 4px 0px rgb(var(--ink-navy))',
- 'pixel-sm': '2px 2px 0px rgb(var(--ink-navy))',
+ 'scrap-card': '4px 6px 12px rgba(44, 58, 79, 0.15)', // Harsh shadow
+ 'scrap-deep': '8px 10px 0px rgba(44, 58, 79, 0.1)', // Hard deep shadow
+ 'scrap-lift': '4px 8px 16px rgba(44, 58, 79, 0.25)', // Lifted state
},
- // ... existing spacing/animation configs can stay or be cleaned up
animation: {
- 'fade-in': 'fadeIn 0.4s ease-out',
- 'fade-out': 'fadeOut 0.3s ease-out',
- 'slide-up': 'slideUp 0.4s ease-out',
- 'slide-down': 'slideDown 0.4s ease-out',
- 'scale-in': 'scaleIn 0.3s ease-out',
- 'bounce-gentle': 'bounceGentle 2s ease-in-out infinite',
- 'pulse-soft': 'pulseSoft 2.5s ease-in-out infinite',
'float': 'float 6s ease-in-out infinite',
- 'shimmer': 'shimmer 2.5s infinite',
+ 'wiggle': 'wiggle 2s ease-in-out infinite',
+ 'lift': 'lift 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards',
},
keyframes: {
- fadeIn: {
- '0%': { opacity: '0' },
- '100%': { opacity: '1' },
- },
- fadeOut: {
- '0%': { opacity: '1' },
- '100%': { opacity: '0' },
- },
- slideUp: {
- '0%': { opacity: '0', transform: 'translateY(24px)' },
- '100%': { opacity: '1', transform: 'translateY(0)' },
- },
- slideDown: {
- '0%': { opacity: '0', transform: 'translateY(-24px)' },
- '100%': { opacity: '1', transform: 'translateY(0)' },
- },
- scaleIn: {
- '0%': { opacity: '0', transform: 'scale(0.95)' },
- '100%': { opacity: '1', transform: 'scale(1)' },
- },
- bounceGentle: {
- '0%, 100%': { transform: 'translateY(0)' },
- '50%': { transform: 'translateY(-4px)' },
- },
- pulseSoft: {
- '0%, 100%': { opacity: '1' },
- '50%': { opacity: '0.7' },
- },
float: {
- '0%, 100%': { transform: 'translateY(0px) rotate(0deg)' },
- '33%': { transform: 'translateY(-10px) rotate(1deg)' },
- '66%': { transform: 'translateY(-5px) rotate(-1deg)' },
+ '0%, 100%': { transform: 'translateY(0)' },
+ '50%': { transform: 'translateY(-10px)' },
},
- shimmer: {
- '0%': { transform: 'translateX(-100%)' },
- '100%': { transform: 'translateX(100%)' },
+ wiggle: {
+ '0%, 100%': { transform: 'rotate(-3deg)' },
+ '50%': { transform: 'rotate(3deg)' },
},
+ lift: {
+ '0%': { transform: 'translateY(0)', boxShadow: '4px 6px 12px rgba(44, 58, 79, 0.15)' },
+ '100%': { transform: 'translateY(-4px) rotate(-1deg)', boxShadow: '6px 12px 20px rgba(44, 58, 79, 0.25)' },
+ }
},
},
},
- plugins: [
- function ({ addUtilities, theme }) {
- const newUtilities = {
- '.text-balance': {
- 'text-wrap': 'balance',
- },
- '.text-pretty': {
- 'text-wrap': 'pretty',
- },
- '.scrollbar-none': {
- '-ms-overflow-style': 'none',
- 'scrollbar-width': 'none',
- },
- '.scrollbar-none::-webkit-scrollbar': {
- 'display': 'none',
- },
- '.perspective': {
- 'perspective': '1200px',
- },
- '.preserve-3d': {
- 'transform-style': 'preserve-3d',
- },
- '.backface-hidden': {
- 'backface-visibility': 'hidden',
- },
- '.writing-vertical': {
- 'writing-mode': 'vertical-rl',
- 'text-orientation': 'mixed',
- },
- }
- addUtilities(newUtilities)
- }
- ],
+ plugins: [],
}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 067fda1..bb0fbfe 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,79 +1,46 @@
-import { defineConfig, loadEnv } from 'vite'
-import react from '@vitejs/plugin-react'
-import { VitePWA } from 'vite-plugin-pwa'
-import path from 'path'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { VitePWA } from "vite-plugin-pwa";
-export default defineConfig(({ mode }) => {
- const env = loadEnv(mode, path.resolve(__dirname, "../.."), "");
- const apiTarget = process.env.VITE_API_PROXY_TARGET || env.VITE_API_PROXY_TARGET || "http://127.0.0.1:8788";
- // Visible at dev startup so proxy target confusion is obvious.
- console.log(`[vite] API proxy target: ${apiTarget}`);
- const isDev = mode === "development";
-
- return {
- envDir: path.resolve(__dirname, "../.."),
- server: {
- watch: {
- // Wrangler mutates these paths during local API requests; ignore to avoid dev full-reloads.
- ignored: ["**/.wrangler/**"],
- },
- proxy: {
- "/api": {
- target: apiTarget,
- changeOrigin: true
- }
- }
- },
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src')
- }
- },
- plugins: [
- react(),
- ...(isDev ? [] : [VitePWA({
- registerType: 'autoUpdate',
- includeAssets: ['icon.svg'],
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ VitePWA({
+ registerType: "autoUpdate",
manifest: {
- name: 'Sanctuary Book Reader',
- short_name: 'Sanctuary',
- description: 'Your personal reading haven. A beautiful, modern EPUB reader.',
- theme_color: '#caa16eff',
- background_color: '#F8F4EC',
- display: 'standalone',
- orientation: 'portrait-primary',
- start_url: '/',
+ name: "Sanctuary",
+ short_name: "Sanctuary",
+ description: "A cozy place for your books.",
+ theme_color: "#FDFBF7", // Pale, buttery cream
+ background_color: "#FDFBF7",
+ display: "standalone",
icons: [
- { src: 'icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any maskable' }
- ]
- },
- workbox: {
- globPatterns: ['**/*.{js,css,html,svg,woff,woff2}'],
- runtimeCaching: [
{
- urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
- handler: 'CacheFirst',
- options: { cacheName: 'google-fonts-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 } }
+ src: "/pwa-192x192.png",
+ sizes: "192x192",
+ type: "image/png",
},
{
- urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
- handler: 'CacheFirst',
- options: { cacheName: 'gstatic-fonts-cache', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 } }
- }
- ]
- }
- })])
- ],
- build: {
- rollupOptions: {
- output: {
- manualChunks: {
- 'vendor-react': ['react', 'react-dom'],
- 'vendor-epub': ['epubjs'],
- 'vendor-ui': ['framer-motion', 'lucide-react'],
- }
- }
- }
- }
- };
-})
+ src: "/pwa-512x512.png",
+ sizes: "512x512",
+ type: "image/png",
+ },
+ ],
+ },
+ }),
+ ],
+ resolve: {
+ alias: {
+ "@": "/src",
+ },
+ },
+ server: {
+ proxy: {
+ "/api": {
+ target: "http://127.0.0.1:8788",
+ changeOrigin: true,
+ },
+ },
+ },
+});
diff --git a/create_dummy_epub.py b/create_dummy_epub.py
new file mode 100644
index 0000000..fc331db
--- /dev/null
+++ b/create_dummy_epub.py
@@ -0,0 +1,50 @@
+import zipfile
+
+def create_epub(filename):
+ with zipfile.ZipFile(filename, 'w') as zf:
+ zf.writestr('mimetype', 'application/epub+zip', compress_type=zipfile.ZIP_STORED)
+ zf.writestr('META-INF/container.xml', '''
+
+
+
+
+ ''')
+ zf.writestr('content.opf', '''
+
+
+ Snoopy's Guide to Life
+ en
+ urn:uuid:12345
+ Charles M. Schulz
+
+
+
+
+
+
+
+
+ ''')
+ zf.writestr('toc.ncx', '''
+
+
+
+
+
+ Snoopy's Guide
+
+
+
+
+ Start
+
+
+
+
+ ''')
+ zf.writestr('page1.html', '
Hello Snoopy! ')
+
+if __name__ == "__main__":
+ create_epub("dummy.epub")
+ print("Created dummy.epub")
diff --git a/debug_populated_failure.png b/debug_populated_failure.png
new file mode 100644
index 0000000..2c41d31
Binary files /dev/null and b/debug_populated_failure.png differ
diff --git a/dummy.epub b/dummy.epub
new file mode 100644
index 0000000..5c6bae0
Binary files /dev/null and b/dummy.epub differ
diff --git a/error_screenshot.png b/error_screenshot.png
new file mode 100644
index 0000000..5f6e671
Binary files /dev/null and b/error_screenshot.png differ
diff --git a/library_empty_mocked.png b/library_empty_mocked.png
new file mode 100644
index 0000000..305e8f3
Binary files /dev/null and b/library_empty_mocked.png differ
diff --git a/library_empty_scrapbook.png b/library_empty_scrapbook.png
new file mode 100644
index 0000000..887250a
Binary files /dev/null and b/library_empty_scrapbook.png differ
diff --git a/library_empty_verified.png b/library_empty_verified.png
new file mode 100644
index 0000000..77794c6
Binary files /dev/null and b/library_empty_verified.png differ
diff --git a/library_populated_mocked.png b/library_populated_mocked.png
new file mode 100644
index 0000000..bffbfad
Binary files /dev/null and b/library_populated_mocked.png differ
diff --git a/verify_scrapbook.py b/verify_scrapbook.py
new file mode 100644
index 0000000..51be4a3
--- /dev/null
+++ b/verify_scrapbook.py
@@ -0,0 +1,93 @@
+import asyncio
+from playwright.async_api import async_playwright, Route
+
+async def run():
+ async with async_playwright() as p:
+ browser = await p.chromium.launch()
+ page = await browser.new_page()
+
+ # Mock the API to return empty list first
+ async def handle_empty_library(route: Route):
+ await route.fulfill(
+ status=200,
+ content_type="application/json",
+ body='[]'
+ )
+
+ # Mock the API to return one book
+ async def handle_populated_library(route: Route):
+ await route.fulfill(
+ status=200,
+ content_type="application/json",
+ body='''[{
+ "id": "book-1",
+ "title": "Snoopy's Guide to Life",
+ "author": "Charles M. Schulz",
+ "coverUrl": "",
+ "progressPercent": 0,
+ "lastLocation": "",
+ "bookmarks": [],
+ "status": "to-read",
+ "favorite": false,
+ "updatedAt": "2023-10-27T10:00:00Z"
+ }]'''
+ )
+
+ try:
+ # ---------------------------------------------------------
+ # TEST 1: Empty State
+ # ---------------------------------------------------------
+ await page.route("**/api/v2/library", handle_empty_library)
+
+ print("Navigating to homepage (Empty State)...")
+ await page.goto("http://localhost:5173")
+
+ # Wait for loading to finish (skeleton to disappear)
+ # The empty state text should appear.
+ try:
+ await page.wait_for_selector("text=The shelves are bare...", timeout=5000)
+ print("SUCCESS: Found empty state text 'The shelves are bare...'")
+ except:
+ print("FAILURE: Did not find empty state text 'The shelves are bare...'")
+ await page.screenshot(path="debug_empty_failure.png")
+
+ await page.screenshot(path="library_empty_mocked.png")
+ print("Screenshot saved: library_empty_mocked.png")
+
+ # ---------------------------------------------------------
+ # TEST 2: Populated State (Bunnies Pick)
+ # ---------------------------------------------------------
+ print("Reloading with populated library (Bunnies Pick)...")
+ # Unroute the previous handler and route the new one
+ await page.unroute("**/api/v2/library")
+ await page.route("**/api/v2/library", handle_populated_library)
+
+ await page.reload()
+
+ # Wait for Bunnies Pick
+ try:
+ # "Bunnies Pick" text in h2
+ # The component renders "Recommended Reading" and "Bunnies' Choice"
+ await page.wait_for_selector("text=Recommended Reading", timeout=5000)
+ print("SUCCESS: Found 'Bunnies Pick' section (Recommended Reading)")
+
+ # Check for book title
+ if await page.is_visible("text=Snoopy's Guide to Life"):
+ print("SUCCESS: Found book 'Snoopy's Guide to Life'")
+ else:
+ print("FAILURE: Did not find book title")
+ except Exception as e:
+ print(f"FAILURE: Did not find 'Bunnies Pick' section. Error: {e}")
+ await page.screenshot(path="debug_populated_failure.png")
+
+ await page.screenshot(path="library_populated_mocked.png")
+ print("Screenshot saved: library_populated_mocked.png")
+
+ except Exception as e:
+ print(f"Error during verification: {e}")
+ await page.screenshot(path="error_fatal.png")
+ finally:
+ await browser.close()
+
+if __name__ == "__main__":
+ asyncio.run(run())