Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 0 additions & 56 deletions apps/web/index.css

This file was deleted.

54 changes: 24 additions & 30 deletions apps/web/index.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://*.clerk.com https://*.clerk.accounts.dev; script-src-elem 'self' 'unsafe-inline' 'unsafe-eval' blob: https://*.clerk.com https://*.clerk.accounts.dev; style-src 'self' 'unsafe-inline' blob: https://fonts.googleapis.com; style-src-elem 'self' 'unsafe-inline' blob: https://fonts.googleapis.com; img-src 'self' blob: data: https:; connect-src 'self' blob: https://*.clerk.com https://*.clerk.accounts.dev wss://*.clerk.com; frame-src 'self' https://*.clerk.com https://*.clerk.accounts.dev; font-src 'self' blob: https://fonts.gstatic.com data:;">
<meta name="description" content="Sanctuary - Your personal reading haven. A beautiful, modern EPUB reader for focused reading." />
<meta name="theme-color" content="#8B7355" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Sanctuary" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/icon.svg" />
<title>Sanctuary - Book Reader</title>
<link rel="stylesheet" href="/index.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
</head>
<body class="antialiased">
<noscript>
<div style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui;background:#faf8f3;color:#2d2a26;padding:2rem;text-align:center;">
<div>
<h1 style="font-size:1.5rem;margin-bottom:0.5rem;">JavaScript Required</h1>
<p style="opacity:0.7;">Please enable JavaScript to use Sanctuary Book Reader.</p>
</div>
</div>
</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Preconnect for faster font loading -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!--
Fonts:
- Fredoka: Headings/Logos (chunky, rounded, playful)
- Sniglet: Headings/Logos (alternative playful)
- Courier Prime: Body/UI Text (vintage typewriter)
- Special Elite: Body/UI Text (alternative vintage typewriter)
- Playfair Display: Accents/Quotes (elegant serif)
-->
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&family=Fredoka:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400;1,700&family=Sniglet:wght@400;800&family=Special+Elite&display=swap" rel="stylesheet" />
<title>Sanctuary</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
114 changes: 54 additions & 60 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ReaderView from "./components/pages/ReaderView";
import SettingsView from "./components/pages/SettingsView";
import StatsView from "./components/pages/StatsView";
import ClerkAuth from "./components/pages/Auth";
import ScrapbookLayout from "./components/layout/ScrapbookLayout";
import { ReaderErrorBoundary } from "./components/ui/ReaderErrorBoundary";

const App: React.FC = () => {
Expand Down Expand Up @@ -53,7 +54,6 @@ const App: React.FC = () => {
}));

// Library & Stats Hooks
// Guest and Clerk users both persist through the API with scoped identities.
const persistent = true;

useBookStoreController({ persistent });
Expand Down Expand Up @@ -143,87 +143,81 @@ const App: React.FC = () => {
const root = document.documentElement;
root.classList.toggle("dark", theme === Theme.DARK);
root.classList.toggle("reduce-motion", reduceMotion);

const bgColor = theme === Theme.DARK ? "#0f0e0d" : "#fefcf8";
document.body.style.backgroundColor = bgColor;
document.body.style.transition = reduceMotion ? "none" : "background-color 0.3s ease";
}, [theme, reduceMotion]);

// Render - Explicit Auth Screen (only when user asks to sign in)
// Render - Explicit Auth Screen (Scrapbook Themed)
if (showAuthScreen && !isLoaded) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-light-primary dark:bg-dark-primary">
<div className="relative mb-6">
<div className="absolute inset-0 bg-gradient-to-br from-light-accent/20 to-amber-500/20 dark:from-dark-accent/20 dark:to-amber-400/20 rounded-3xl blur-3xl scale-150" />
<div className="relative w-20 h-20 rounded-3xl bg-gradient-to-br from-light-accent to-amber-600 dark:from-dark-accent dark:to-amber-500 flex items-center justify-center shadow-2xl">
<BookOpen className="w-9 h-9 text-white animate-pulse-soft" strokeWidth={1.5} />
</div>
</div>
<div className="text-center space-y-2">
<h2 className="text-xl font-semibold text-light-text dark:text-dark-text">Sanctuary</h2>
<p className="text-sm text-light-text-muted dark:text-dark-text-muted">Preparing your reading sanctuary...</p>
<ScrapbookLayout>
<div className="min-h-screen flex flex-col items-center justify-center pointer-events-none">
<div className="bg-white p-8 rounded-full shadow-scrap-deep animate-pulse-soft border-4 border-scrap-navy pointer-events-auto">
<BookOpen className="w-12 h-12 text-scrap-navy" strokeWidth={2} />
</div>
<p className="mt-6 text-scrap-navy font-head text-xl font-bold bg-scrap-cream px-4 py-1 rounded-lg border border-scrap-navy shadow-sm transform -rotate-2">
Gathering supplies...
</p>
</div>
</div>
</ScrapbookLayout>
);
}

if (showAuthScreen) {
return <ClerkAuth onContinueAsGuest={() => {
setIsGuest(true);
setShowAuthScreen(false);
}} />;
return (
<ScrapbookLayout>
<ClerkAuth onContinueAsGuest={() => {
setIsGuest(true);
setShowAuthScreen(false);
}} />
</ScrapbookLayout>
);
}

const isReader = view === View.READER;

// Render - App
if (isReader && selectedBook) {
return (
<ReaderErrorBoundary onRecover={() => { void handleCloseReader(); }} resetKey={selectedBook.id}>
<ReaderView
book={selectedBook}
onClose={handleCloseReader}
onUpdateProgress={handleReaderProgress}
onAddBookmark={handleAddBookmark}
onRemoveBookmark={handleRemoveBookmark}
getBookContent={getBookContent}
/>
</ReaderErrorBoundary>
);
}

// Render - Main App (Scrapbook Layout)
return (
<div className={`min-h-screen font-sans bg-light-primary dark:bg-dark-primary text-light-text dark:text-dark-text transition-colors duration-300 ${isReader ? "immersive-layout" : "standard-layout"}`}>
<ScrapbookLayout view={view}>
{/* Header */}
{!isReader && (
<Header
theme={theme}
onToggleTheme={toggleTheme}
searchTerm={searchTerm}
onSearch={setSearchTerm}
isGuest={isGuest}
onShowLogin={isGuest ? handleShowLogin : undefined}
onSignOut={isSignedIn ? handleSignOut : undefined}
userEmail={user?.primaryEmailAddress?.emailAddress}
userImage={user?.imageUrl}
/>
)}

{/* Main Content */}
<main className={`relative ${isReader ? "reader-main" : "standard-main"}`}>
<div className={`${isReader ? "" : "page-shell animate-fadeIn"}`}>
<Header
theme={theme}
onToggleTheme={toggleTheme}
searchTerm={searchTerm}
onSearch={setSearchTerm}
isGuest={isGuest}
onShowLogin={isGuest ? handleShowLogin : undefined}
onSignOut={isSignedIn ? handleSignOut : undefined}
userEmail={user?.primaryEmailAddress?.emailAddress}
userImage={user?.imageUrl}
/>

{/* Main Content Area */}
<main className="flex-1 w-full max-w-7xl mx-auto px-4 pb-32">
{view === View.LIBRARY && (
<LibraryGrid
onSelectBook={handleSelectBook}
/>
<LibraryGrid onSelectBook={handleSelectBook} />
)}
{/* We keep Stats and Settings as is for now, they will inherit global font/color styles but might need specific layout tweaks later if requested. The prompt focused on Library/Empty State. */}
{view === View.SETTINGS && <SettingsView />}
{view === View.STATS && <StatsView />}
{view === View.READER && selectedBook && (
<ReaderErrorBoundary onRecover={() => { void handleCloseReader(); }} resetKey={selectedBook.id}>
<ReaderView
book={selectedBook}
onClose={handleCloseReader}
onUpdateProgress={handleReaderProgress}
onAddBookmark={handleAddBookmark}
onRemoveBookmark={handleRemoveBookmark}
getBookContent={getBookContent}
/>
</ReaderErrorBoundary>
)}
</div>
</main>

{/* Navigation */}
{!isReader && (
<Navigation activeView={view} onNavigate={setView} isReaderActive={!!selectedBookId} />
)}
</div>
<Navigation activeView={view} onNavigate={setView} isReaderActive={!!selectedBookId} />
</ScrapbookLayout>
);
};

Expand Down
87 changes: 87 additions & 0 deletions apps/web/src/components/layout/ScrapbookLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { ReactNode } from "react";
import { motion } from "framer-motion";

interface ScrapbookLayoutProps {
children: ReactNode;
view?: string;
}

const ScrapbookLayout: React.FC<ScrapbookLayoutProps> = ({ children, view }) => {
return (
<div className="relative min-h-screen w-full bg-scrap-cream overflow-hidden">
{/*
============================================================
LAYER 0: Base Texture
============================================================
*/}
<div
className="fixed inset-0 pointer-events-none z-0 opacity-40 mix-blend-multiply"
style={{ backgroundImage: "var(--noise-svg)" }}
/>

{/*
============================================================
LAYER 1: The Big Scraps (Collage Background)
============================================================
*/}
<div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">

{/* Top Left: Celestial Star Map (Abstracted) */}
<motion.div
initial={{ opacity: 0, rotate: -5 }}
animate={{ opacity: 1, rotate: 0 }}
transition={{ duration: 1.5 }}
className="absolute -top-20 -left-20 w-96 h-96 bg-scrap-navy opacity-10 rounded-full blur-3xl transform rotate-12"
/>
<div className="absolute top-10 left-10 w-64 h-64 border-2 border-scrap-navy/10 rounded-full opacity-20 animate-spin-slow" style={{ animationDuration: '60s' }}>
<div className="absolute top-0 left-1/2 w-2 h-2 bg-scrap-navy rounded-full transform -translate-x-1/2" />
<div className="absolute bottom-0 left-1/2 w-1 h-1 bg-scrap-navy rounded-full transform -translate-x-1/2" />
</div>

{/* Right Side: Vintage Sheet Music (CSS Pattern) */}
<motion.div
initial={{ x: 100, opacity: 0 }}
animate={{ x: 0, opacity: 0.6 }}
className="absolute top-1/4 -right-12 w-80 h-[600px] bg-scrap-kraft/30 rotate-3 transform shadow-lg"
style={{
clipPath: "polygon(2% 0%, 100% 0%, 100% 100%, 0% 100%, 5% 90%, 0% 80%, 3% 70%, 0% 60%, 4% 50%, 0% 40%, 3% 30%, 0% 20%, 5% 10%)",
backgroundImage: "repeating-linear-gradient(transparent, transparent 19px, rgba(44, 58, 79, 0.1) 20px)"
}}
>
{/* Music Notes Scatter */}
<div className="absolute top-20 left-10 text-4xl opacity-20 rotate-12">𝄞</div>
<div className="absolute top-40 left-20 text-3xl opacity-20 -rotate-12">𝅘𝅥𝅮</div>
<div className="absolute bottom-20 left-12 text-3xl opacity-20 rotate-6">𝅘𝅥𝅯</div>
</motion.div>

{/* Bottom Left: Night Sky / Navy Paper */}
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="absolute -bottom-24 -left-12 w-[500px] h-[400px] bg-scrap-navy transform -rotate-2 shadow-scrap-deep"
style={{ clipPath: "polygon(0% 10%, 10% 0%, 100% 5%, 95% 100%, 0% 100%)" }}
>
{/* Stars */}
<div className="absolute top-10 right-20 text-scrap-cream opacity-40 text-xl">✦</div>
<div className="absolute top-24 right-40 text-scrap-cream opacity-20 text-sm">✧</div>
<div className="absolute top-40 right-10 text-scrap-cream opacity-30 text-lg">★</div>
<div className="absolute top-1/2 left-1/3 w-32 h-32 border border-scrap-cream/10 rounded-full" />
</motion.div>

{/* Center/Random: Coffee Stain */}
<div className="absolute top-1/2 left-1/4 w-40 h-40 border-8 border-[#5D4037]/10 rounded-full transform scale-y-75 -rotate-12 mix-blend-multiply pointer-events-none blur-sm" />
</div>

{/*
============================================================
LAYER 2: Content Container
============================================================
*/}
<div className="relative z-10 w-full min-h-screen flex flex-col">
{children}
</div>
</div>
);
};

export default ScrapbookLayout;
Loading