Upload files to "/"

This commit is contained in:
2026-09-14 23:34:36 +08:00
parent a26bbecbd1
commit 1e893db246
5 changed files with 631 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import { Navbar, NavTab, AgeGroup } from './components/Navbar';
import { StoryReader } from './components/StoryReader';
import { StoryboardView } from './components/StoryboardView';
import { ArtDirectionView } from './components/ArtDirectionView';
import { CharacterVault } from './components/CharacterVault';
import { AdaptabilityView } from './components/AdaptabilityView';
import { FormatStrategyView } from './components/FormatStrategyView';
import { ActivityCorner } from './components/ActivityCorner';
import { PitchDeckModal } from './components/PitchDeckModal';
import { BOOK_METADATA } from './data/storyData';
import { soundManager } from './utils/audio';
import { Heart } from 'lucide-react';
export function App() {
const [activeTab, setActiveTab] = useState<NavTab>('reader');
const [ageGroup, setAgeGroup] = useState<AgeGroup>('pictureBook');
const [isBedtimeMode, setIsBedtimeMode] = useState<boolean>(false);
const [soundEnabled, setSoundEnabled] = useState<boolean>(true);
const [isPitchDeckOpen, setIsPitchDeckOpen] = useState<boolean>(false);
const [storyboardSceneId, setStoryboardSceneId] = useState<number>(1);
// Sync global .dark class on <html> with Bedtime Mode so all dark: variants apply
useEffect(() => {
const root = document.documentElement;
if (isBedtimeMode) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [isBedtimeMode]);
const handleExploreStoryboard = (sceneId: number) => {
soundManager.playPop();
setStoryboardSceneId(sceneId);
setActiveTab('storyboard');
};
return (
<div className={`min-h-screen flex flex-col font-reading transition-colors duration-500 ${
isBedtimeMode
? 'bg-slate-950 text-indigo-50'
: 'bg-[#faf6ee] text-stone-900'
}`}>
{/* Top Navigation */}
<Navbar
activeTab={activeTab}
setActiveTab={setActiveTab}
ageGroup={ageGroup}
setAgeGroup={setAgeGroup}
isBedtimeMode={isBedtimeMode}
setIsBedtimeMode={setIsBedtimeMode}
soundEnabled={soundEnabled}
setSoundEnabled={setSoundEnabled}
onOpenPitchDeck={() => setIsPitchDeckOpen(true)}
/>
{/* Main View Router */}
<main className="flex-1">
{activeTab === 'reader' && (
<StoryReader
ageGroup={ageGroup}
setAgeGroup={setAgeGroup}
isBedtimeMode={isBedtimeMode}
soundEnabled={soundEnabled}
onExploreStoryboard={handleExploreStoryboard}
/>
)}
{activeTab === 'storyboard' && (
<StoryboardView
isBedtimeMode={isBedtimeMode}
selectedSceneId={storyboardSceneId}
/>
)}
{activeTab === 'art' && (
<ArtDirectionView
isBedtimeMode={isBedtimeMode}
/>
)}
{activeTab === 'characters' && (
<CharacterVault
isBedtimeMode={isBedtimeMode}
/>
)}
{activeTab === 'adaptability' && (
<AdaptabilityView
isBedtimeMode={isBedtimeMode}
/>
)}
{activeTab === 'publishing' && (
<FormatStrategyView
isBedtimeMode={isBedtimeMode}
onOpenPitchDeck={() => setIsPitchDeckOpen(true)}
/>
)}
{activeTab === 'activities' && (
<ActivityCorner
isBedtimeMode={isBedtimeMode}
/>
)}
</main>
{/* Footer */}
<footer className={`border-t py-8 px-4 sm:px-6 lg:px-8 transition-colors ${
isBedtimeMode
? 'bg-indigo-950/70 border-indigo-900 text-indigo-200'
: 'bg-[#f7f2e7] border-amber-200/80 text-stone-700'
}`}>
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4 text-xs">
<div className="flex items-center gap-2">
<span className="text-xl">🦔</span>
<div>
<p className="font-heading font-bold text-sm text-stone-900 dark:text-white">
{BOOK_METADATA.title}
</p>
<p className="text-[11px] font-medium text-stone-600 dark:text-indigo-300">
Children's Storybook Concept & Interactive Story Experience
</p>
</div>
</div>
<div className="flex flex-wrap items-center justify-center gap-4 text-xs font-semibold">
<button
onClick={() => { soundManager.playPop(); setActiveTab('reader'); }}
className="hover:text-amber-600 transition-colors"
>
Story Reader
</button>
<button
onClick={() => { soundManager.playPop(); setActiveTab('storyboard'); }}
className="hover:text-amber-600 transition-colors"
>
Storyboard
</button>
<button
onClick={() => { soundManager.playPop(); setActiveTab('art'); }}
className="hover:text-amber-600 transition-colors"
>
Art Direction
</button>
<button
onClick={() => { soundManager.playPop(); setActiveTab('characters'); }}
className="hover:text-amber-600 transition-colors"
>
Character Vault
</button>
<button
onClick={() => { soundManager.playPop(); setActiveTab('adaptability'); }}
className="hover:text-amber-600 transition-colors"
>
Age Matrix
</button>
<button
onClick={() => { soundManager.playPop(); setActiveTab('publishing'); }}
className="hover:text-amber-600 transition-colors"
>
Publishing Specs
</button>
<button
onClick={() => { soundManager.playPop(); setIsPitchDeckOpen(true); }}
className="text-amber-700 dark:text-amber-400 font-bold hover:underline"
>
Editorial Pitch Deck
</button>
</div>
<div className="flex items-center gap-1.5 text-[11px] font-semibold text-stone-700 dark:text-indigo-200">
<span>Crafted with</span>
<Heart className="w-3.5 h-3.5 text-rose-500 fill-rose-500" />
<span>by</span>
<span className="font-heading font-bold text-stone-900 dark:text-white tracking-wide">aimirah</span>
<span className="text-stone-500 dark:text-indigo-400">|</span>
<span className="font-heading font-bold text-amber-800 dark:text-amber-300 tracking-wide">mi4inc</span>
</div>
</div>
</footer>
{/* Editorial Pitch Deck Modal */}
<PitchDeckModal
isOpen={isPitchDeckOpen}
onClose={() => setIsPitchDeckOpen(false)}
/>
</div>
);
}
export default App;
+250
View File
@@ -0,0 +1,250 @@
// Web Audio API Synthesizer for Children's Storybook
class SoundEffectsManager {
private ctx: AudioContext | null = null;
public enabled: boolean = true;
private initCtx() {
if (!this.ctx && typeof window !== 'undefined') {
const AudioCtx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
if (AudioCtx) {
this.ctx = new AudioCtx();
}
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
// Soft cheerful pop
playPop() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
const now = this.ctx.currentTime;
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.08);
gain.gain.setValueAtTime(0.18, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.1);
}
// Pentatonic magical chime
playChime() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
notes.forEach((freq, idx) => {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
const now = this.ctx.currentTime + idx * 0.07;
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.4);
});
}
// Soft gentle raindrop plop
playRain() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const drops = [700, 1100, 850];
drops.forEach((freq, i) => {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime + i * 0.08;
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now);
osc.frequency.exponentialRampToValueAtTime(freq * 0.5, now + 0.07);
gain.gain.setValueAtTime(0.14, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.15);
});
}
// Harp strum
playHarp() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const notes = [392.00, 523.25, 659.25, 783.99, 1046.5]; // G4, C5, E5, G5, C6
notes.forEach((freq, i) => {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime + i * 0.05;
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.65);
});
}
// Flutter / whoosh
playFlutter() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime;
osc.type = 'sine';
osc.frequency.setValueAtTime(220, now);
osc.frequency.linearRampToValueAtTime(440, now + 0.1);
osc.frequency.linearRampToValueAtTime(260, now + 0.2);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.25);
}
// Playful giggle / chirp
playGiggle() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const notes = [587.33, 739.99, 587.33, 880.00];
notes.forEach((freq, i) => {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime + i * 0.06;
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0.09, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.12);
});
}
// Page turn sound
playPageTurn() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const bufferSize = this.ctx.sampleRate * 0.15;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1200;
const gain = this.ctx.createGain();
const now = this.ctx.currentTime;
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.14);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start(now);
}
// Star Celebration fanfare
playStarCelebration() {
if (!this.enabled) return;
this.initCtx();
if (!this.ctx) return;
const notes = [523.25, 659.25, 783.99, 1046.50, 1318.51];
notes.forEach((freq, i) => {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const now = this.ctx.currentTime + i * 0.08;
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.5);
});
}
playSound(type: 'pop' | 'chime' | 'rain' | 'harp' | 'flutter' | 'giggle') {
switch (type) {
case 'pop': this.playPop(); break;
case 'chime': this.playChime(); break;
case 'rain': this.playRain(); break;
case 'harp': this.playHarp(); break;
case 'flutter': this.playFlutter(); break;
case 'giggle': this.playGiggle(); break;
default: this.playPop();
}
}
}
export const soundManager = new SoundEffectsManager();
+80
View File
@@ -0,0 +1,80 @@
@import "tailwindcss";
/* Class-based dark variant so Bedtime Mode toggle drives every dark: style */
@custom-variant dark (&:where(.dark, .dark *));
@layer base {
body {
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #fbf8f2;
color: #2c2523;
overflow-x: hidden;
}
h1, h2, h3, .font-heading {
font-family: 'Fredoka', 'Quicksand', sans-serif;
}
.font-hand {
font-family: 'Patrick Hand', cursive;
}
.font-reading {
font-family: 'Quicksand', 'Nunito', sans-serif;
}
}
@layer components {
/* Custom whimsical storybook effects */
.book-shadow {
box-shadow: 0 20px 40px -15px rgba(45, 25, 20, 0.22),
0 0 0 1px rgba(0, 0, 0, 0.05),
0 1px 3px rgba(0, 0, 0, 0.1);
}
.book-spine {
box-shadow: inset 18px 0 30px -10px rgba(0, 0, 0, 0.15),
inset -4px 0 8px rgba(0, 0, 0, 0.05);
}
.paper-texture {
background-color: #fdfaf5;
background-image: radial-gradient(#ece4d4 0.75px, transparent 0.75px), radial-gradient(#ece4d4 0.75px, #fdfaf5 0.75px);
background-size: 30px 30px;
background-position: 0 0, 15px 15px;
}
/* Dark paper texture for Bedtime Mode */
.dark .paper-texture {
background-color: #0f172a;
background-image: radial-gradient(rgba(99, 102, 241, 0.25) 0.75px, transparent 0.75px), radial-gradient(rgba(99, 102, 241, 0.18) 0.75px, #0f172a 0.75px);
}
.animate-float-slow {
animation: floatSlow 5s ease-in-out infinite;
}
.animate-pulse-gentle {
animation: gentlePulse 3.5s ease-in-out infinite;
}
.animate-sparkle {
animation: shimmerSparkle 3s ease-in-out infinite;
}
}
@keyframes floatSlow {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-8px) rotate(1.5deg); }
}
@keyframes gentlePulse {
0%, 100% { opacity: 0.9; transform: scale(1); }
50% { opacity: 1; transform: scale(1.03); }
}
@keyframes shimmerSparkle {
0% { transform: scale(0.8) rotate(0deg); opacity: 0.4; }
50% { transform: scale(1.15) rotate(90deg); opacity: 1; }
100% { transform: scale(0.8) rotate(180deg); opacity: 0.4; }
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
+98
View File
@@ -0,0 +1,98 @@
export class StorySpeechNarrator {
private synth: SpeechSynthesis | null = null;
private currentUtterance: SpeechSynthesisUtterance | null = null;
private onEndCallback: (() => void) | null = null;
private onWordCallback: ((wordIndex: number) => void) | null = null;
constructor() {
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
this.synth = window.speechSynthesis;
}
}
public isAvailable(): boolean {
return this.synth !== null;
}
public speak(
text: string,
options?: {
rate?: number;
pitch?: number;
onEnd?: () => void;
onWord?: (index: number) => void;
}
) {
if (!this.synth) return;
this.stop();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = options?.rate ?? 0.9; // Slightly slower for children's story cadence
utterance.pitch = options?.pitch ?? 1.05; // Slightly warmer/friendly pitch
// Find warm friendly voice
const voices = this.synth.getVoices();
const naturalVoice = voices.find(v =>
v.lang.startsWith('en') &&
(v.name.includes('Natural') || v.name.includes('Samantha') || v.name.includes('Karen') || v.name.includes('Daniel') || v.name.includes('Google'))
) || voices.find(v => v.lang.startsWith('en'));
if (naturalVoice) {
utterance.voice = naturalVoice;
}
this.onEndCallback = options?.onEnd || null;
this.onWordCallback = options?.onWord || null;
utterance.onend = () => {
this.currentUtterance = null;
if (this.onEndCallback) this.onEndCallback();
};
utterance.onerror = () => {
this.currentUtterance = null;
if (this.onEndCallback) this.onEndCallback();
};
let wordCount = 0;
utterance.onboundary = (e) => {
if (e.name === 'word') {
if (this.onWordCallback) {
this.onWordCallback(wordCount);
}
wordCount++;
}
};
this.currentUtterance = utterance;
this.synth.speak(utterance);
}
public stop() {
if (!this.synth) return;
this.synth.cancel();
this.currentUtterance = null;
}
public pause() {
if (this.synth) {
this.synth.pause();
}
}
public resume() {
if (this.synth) {
this.synth.resume();
}
}
public isSpeaking(): boolean {
return this.synth ? this.synth.speaking : false;
}
public getCurrentUtterance(): SpeechSynthesisUtterance | null {
return this.currentUtterance;
}
}
export const speechNarrator = new StorySpeechNarrator();