ez-reads 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "paper-site-template",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1"
13
+ },
14
+ "devDependencies": {
15
+ "@tailwindcss/typography": "^0.5.14",
16
+ "@vitejs/plugin-react": "^4.3.1",
17
+ "autoprefixer": "^10.4.20",
18
+ "postcss": "^8.4.41",
19
+ "tailwindcss": "^3.4.10",
20
+ "vite": "^5.4.2"
21
+ }
22
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect } from 'react';
2
+ import paper from './data/paper.json';
3
+ import Hero from './components/Hero';
4
+ import Analogy from './components/Analogy';
5
+ import Abstract from './components/Abstract';
6
+ import Stats from './components/Stats';
7
+ import KeyContributions from './components/KeyContributions';
8
+ import Methodology from './components/Methodology';
9
+ import Figures from './components/Figures';
10
+ import Results from './components/Results';
11
+ import Significance from './components/Significance';
12
+ import Limitations from './components/Limitations';
13
+ import Glossary from './components/Glossary';
14
+ import Footer from './components/Footer';
15
+ import ChatAssistant from './components/ChatAssistant';
16
+
17
+ export default function App() {
18
+ useEffect(() => {
19
+ const root = document.documentElement;
20
+ if (paper.color_theme) {
21
+ root.style.setProperty('--color-primary', paper.color_theme.primary);
22
+ root.style.setProperty('--color-accent', paper.color_theme.accent);
23
+ }
24
+
25
+ // Observe all animated elements
26
+ const observer = new IntersectionObserver(
27
+ (entries) => {
28
+ entries.forEach((entry) => {
29
+ if (entry.isIntersecting) {
30
+ entry.target.classList.add('visible');
31
+ }
32
+ });
33
+ },
34
+ { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
35
+ );
36
+
37
+ document.querySelectorAll('.fade-in, .slide-left, .slide-right, .scale-in, .stagger-children')
38
+ .forEach((el) => observer.observe(el));
39
+
40
+ return () => observer.disconnect();
41
+ }, []);
42
+
43
+ return (
44
+ <div className="min-h-screen bg-gray-50 text-gray-900">
45
+ <Hero title={paper.title} authors={paper.authors} tldr={paper.tldr} publishedDate={paper.publishedDate} />
46
+ <main className="max-w-5xl mx-auto px-6 py-16 space-y-20">
47
+ <Analogy text={paper.analogy} />
48
+ <Abstract text={paper.abstract_simplified} />
49
+ <Stats items={paper.stats} />
50
+ <KeyContributions items={paper.key_contributions} />
51
+ <Methodology methodology={paper.methodology} />
52
+ <Figures items={paper.figures} />
53
+ <Results items={paper.results} />
54
+ <Significance text={paper.significance} />
55
+ <Limitations items={paper.limitations} />
56
+ <Glossary items={paper.glossary} />
57
+ </main>
58
+ <Footer url={paper.url} />
59
+ <ChatAssistant paper={paper} />
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,18 @@
1
+ export default function Abstract({ text }) {
2
+ return (
3
+ <section className="fade-in">
4
+ <div className="flex items-center gap-3 mb-4">
5
+ <div
6
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
7
+ style={{ backgroundColor: 'var(--color-primary)' }}
8
+ >
9
+ ๐Ÿ“–
10
+ </div>
11
+ <h2 className="text-2xl font-bold">Overview</h2>
12
+ </div>
13
+ <div className="bg-white rounded-2xl p-8 shadow-sm border border-gray-100 primary-glow">
14
+ <p className="text-lg leading-relaxed text-gray-700">{text}</p>
15
+ </div>
16
+ </section>
17
+ );
18
+ }
@@ -0,0 +1,20 @@
1
+ export default function Analogy({ text }) {
2
+ if (!text) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-2xl p-8 border border-indigo-200/60 relative overflow-hidden">
7
+ <div className="absolute top-4 right-4 text-6xl opacity-10">๐Ÿ’ก</div>
8
+ <div className="flex items-start gap-4">
9
+ <span className="text-3xl shrink-0 mt-1">๐Ÿ”‘</span>
10
+ <div>
11
+ <h2 className="text-lg font-bold text-indigo-800 mb-2">
12
+ The Intuition
13
+ </h2>
14
+ <p className="text-lg leading-relaxed text-indigo-900/80">{text}</p>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </section>
19
+ );
20
+ }
@@ -0,0 +1,155 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+
3
+ function buildSystemPrompt(paper) {
4
+ const sections = [`You are a helpful research assistant for the paper: "${paper.title}".`];
5
+ if (paper.abstract_simplified) sections.push(`Abstract: ${paper.abstract_simplified}`);
6
+ if (paper.methodology) sections.push(`Methodology: ${paper.methodology}`);
7
+ if (paper.key_contributions?.length) sections.push(`Key contributions:\n${paper.key_contributions.map((c, i) => `${i + 1}. ${c}`).join('\n')}`);
8
+ if (paper.results?.length) sections.push(`Results:\n${paper.results.map((r, i) => `${i + 1}. ${r}`).join('\n')}`);
9
+ if (paper.significance) sections.push(`Significance: ${paper.significance}`);
10
+ if (paper.glossary?.length) sections.push(`Glossary:\n${paper.glossary.map(g => `- ${g.term}: ${g.definition}`).join('\n')}`);
11
+ sections.push('Answer questions about this paper as if you are explaining to a five-year-old: use simple words, short sentences, fun analogies, and relatable examples. But do NOT dumb down the technical content โ€” keep all the important details and complexity, just make them easy to understand. If the user asks something not covered in the paper, say so.');
12
+ return sections.join('\n\n');
13
+ }
14
+
15
+ export default function ChatAssistant({ paper }) {
16
+ const [open, setOpen] = useState(false);
17
+ const [messages, setMessages] = useState([]);
18
+ const [input, setInput] = useState('');
19
+ const [loading, setLoading] = useState(false);
20
+ const scrollRef = useRef(null);
21
+
22
+ if (!paper.groqApiKey) return null;
23
+
24
+ useEffect(() => {
25
+ if (scrollRef.current) {
26
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
27
+ }
28
+ }, [messages]);
29
+
30
+ const sendMessage = async () => {
31
+ const text = input.trim();
32
+ if (!text || loading) return;
33
+
34
+ const userMsg = { role: 'user', content: text };
35
+ const newMessages = [...messages, userMsg];
36
+ setMessages(newMessages);
37
+ setInput('');
38
+ setLoading(true);
39
+
40
+ try {
41
+ const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ 'Authorization': `Bearer ${paper.groqApiKey}`,
46
+ },
47
+ body: JSON.stringify({
48
+ model: 'qwen/qwen3-32b',
49
+ messages: [
50
+ { role: 'system', content: buildSystemPrompt(paper) },
51
+ ...newMessages,
52
+ ],
53
+ temperature: 0.7,
54
+ max_tokens: 1024,
55
+ }),
56
+ });
57
+
58
+ const data = await res.json();
59
+ const raw = data.choices?.[0]?.message?.content || 'Sorry, I could not generate a response.';
60
+ const reply = raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
61
+ setMessages((prev) => [...prev, { role: 'assistant', content: reply }]);
62
+ } catch {
63
+ setMessages((prev) => [...prev, { role: 'assistant', content: 'Something went wrong. Please try again.' }]);
64
+ } finally {
65
+ setLoading(false);
66
+ }
67
+ };
68
+
69
+ const handleKeyDown = (e) => {
70
+ if (e.key === 'Enter' && !e.shiftKey) {
71
+ e.preventDefault();
72
+ sendMessage();
73
+ }
74
+ };
75
+
76
+ return (
77
+ <>
78
+ {/* Floating button */}
79
+ <button
80
+ onClick={() => setOpen(!open)}
81
+ className="chat-fab"
82
+ aria-label="Chat about this paper"
83
+ >
84
+ {open ? (
85
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
86
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
87
+ </svg>
88
+ ) : (
89
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
90
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
91
+ </svg>
92
+ )}
93
+ </button>
94
+
95
+ {/* Chat panel */}
96
+ {open && (
97
+ <div className="chat-panel">
98
+ {/* Header */}
99
+ <div className="chat-header">
100
+ <div className="chat-header-title">
101
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
102
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
103
+ </svg>
104
+ <span>Ask about this paper</span>
105
+ </div>
106
+ <button onClick={() => setOpen(false)} className="chat-close-btn" aria-label="Close chat">
107
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
108
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
109
+ </svg>
110
+ </button>
111
+ </div>
112
+
113
+ {/* Messages */}
114
+ <div className="chat-messages" ref={scrollRef}>
115
+ {messages.length === 0 && (
116
+ <div className="chat-empty">
117
+ <p>Ask me anything about this paper!</p>
118
+ </div>
119
+ )}
120
+ {messages.map((msg, i) => (
121
+ <div key={i} className={`chat-bubble ${msg.role}`}>
122
+ {msg.content}
123
+ </div>
124
+ ))}
125
+ {loading && (
126
+ <div className="chat-bubble assistant">
127
+ <span className="typing-indicator">
128
+ <span /><span /><span />
129
+ </span>
130
+ </div>
131
+ )}
132
+ </div>
133
+
134
+ {/* Input */}
135
+ <div className="chat-input-area">
136
+ <input
137
+ type="text"
138
+ value={input}
139
+ onChange={(e) => setInput(e.target.value)}
140
+ onKeyDown={handleKeyDown}
141
+ placeholder="Type a question..."
142
+ className="chat-input"
143
+ disabled={loading}
144
+ />
145
+ <button onClick={sendMessage} disabled={loading || !input.trim()} className="chat-send-btn" aria-label="Send message">
146
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
147
+ <line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" />
148
+ </svg>
149
+ </button>
150
+ </div>
151
+ </div>
152
+ )}
153
+ </>
154
+ );
155
+ }
@@ -0,0 +1,63 @@
1
+ import { useState } from 'react';
2
+
3
+ function FigureCard({ fig }) {
4
+ const [failed, setFailed] = useState(false);
5
+
6
+ if (failed) return null;
7
+
8
+ return (
9
+ <div className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
10
+ <div className="relative bg-gray-50 flex items-center justify-center" style={{ minHeight: '120px' }}>
11
+ <img
12
+ src={fig.url}
13
+ alt={fig.caption || ''}
14
+ className="w-full object-contain"
15
+ style={{ maxHeight: '480px' }}
16
+ onError={() => setFailed(true)}
17
+ />
18
+ </div>
19
+ <div className="p-5">
20
+ <div className="flex items-center gap-2 mb-2">
21
+ <span
22
+ className="text-xs font-semibold px-2.5 py-0.5 rounded-full"
23
+ style={{
24
+ backgroundColor: 'color-mix(in srgb, var(--color-primary) 12%, white)',
25
+ color: 'var(--color-primary)',
26
+ }}
27
+ >
28
+ {fig.category || 'Figure'}
29
+ </span>
30
+ </div>
31
+ {fig.caption && (
32
+ <p className="text-gray-500 text-xs leading-relaxed mb-2">{fig.caption}</p>
33
+ )}
34
+ {fig.explanation && (
35
+ <p className="text-gray-700 text-sm leading-relaxed">{fig.explanation}</p>
36
+ )}
37
+ </div>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export default function Figures({ items }) {
43
+ if (!items?.length) return null;
44
+
45
+ return (
46
+ <section className="fade-in">
47
+ <div className="flex items-center gap-3 mb-6">
48
+ <div
49
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
50
+ style={{ backgroundColor: 'var(--color-primary)' }}
51
+ >
52
+ ๐Ÿ“Š
53
+ </div>
54
+ <h2 className="text-2xl font-bold">Key Figures</h2>
55
+ </div>
56
+ <div className="space-y-6 stagger-children">
57
+ {items.map((fig, i) => (
58
+ <FigureCard key={fig.id || i} fig={fig} />
59
+ ))}
60
+ </div>
61
+ </section>
62
+ );
63
+ }
@@ -0,0 +1,22 @@
1
+ export default function Footer({ url }) {
2
+ return (
3
+ <footer className="border-t border-gray-200 py-12 px-6">
4
+ <div className="max-w-5xl mx-auto text-center space-y-4">
5
+ {url && (
6
+ <a
7
+ href={url}
8
+ target="_blank"
9
+ rel="noopener noreferrer"
10
+ className="inline-flex items-center gap-2 font-semibold text-lg px-6 py-3 rounded-full text-white transition-transform hover:scale-105 shadow-lg"
11
+ style={{ backgroundColor: 'var(--color-primary)' }}
12
+ >
13
+ ๐Ÿ“„ Read the Original Paper โ†’
14
+ </a>
15
+ )}
16
+ <p className="text-gray-400 text-sm">
17
+ Made with Easy Reads ยท Explained like you're five ๐Ÿง’
18
+ </p>
19
+ </div>
20
+ </footer>
21
+ );
22
+ }
@@ -0,0 +1,36 @@
1
+ export default function Glossary({ items }) {
2
+ if (!items?.length) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="flex items-center gap-3 mb-6">
7
+ <div
8
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
9
+ style={{ backgroundColor: 'var(--color-primary)' }}
10
+ >
11
+ ๐Ÿ“š
12
+ </div>
13
+ <h2 className="text-2xl font-bold">Key Concepts</h2>
14
+ </div>
15
+ <div className="grid sm:grid-cols-2 gap-4 stagger-children">
16
+ {items.map((item, i) => (
17
+ <div
18
+ key={i}
19
+ className="glossary-card bg-white rounded-2xl p-5 shadow-sm border border-gray-100"
20
+ >
21
+ <dt
22
+ className="font-bold text-sm mb-2 inline-block px-3 py-1 rounded-full"
23
+ style={{
24
+ color: 'var(--color-primary)',
25
+ backgroundColor: 'color-mix(in srgb, var(--color-primary) 8%, white)',
26
+ }}
27
+ >
28
+ {item.term}
29
+ </dt>
30
+ <dd className="text-gray-600 text-sm leading-relaxed">{item.definition}</dd>
31
+ </div>
32
+ ))}
33
+ </div>
34
+ </section>
35
+ );
36
+ }
@@ -0,0 +1,56 @@
1
+ function formatDate(dateStr) {
2
+ if (!dateStr) return null;
3
+ const d = new Date(dateStr + (dateStr.length === 10 ? 'T00:00:00' : ''));
4
+ if (isNaN(d)) return null;
5
+ return d.toLocaleDateString('en-US', {
6
+ year: 'numeric',
7
+ month: 'long',
8
+ day: 'numeric',
9
+ });
10
+ }
11
+
12
+ export default function Hero({ title, authors, tldr, publishedDate }) {
13
+ const formattedDate = formatDate(publishedDate);
14
+
15
+ return (
16
+ <header className="gradient-hero text-white py-24 px-6">
17
+ <div className="relative z-10 max-w-5xl mx-auto">
18
+ <div className="flex flex-wrap items-center gap-3 mb-6">
19
+ <div className="inline-block bg-white/10 backdrop-blur-sm rounded-full px-4 py-1.5 text-sm font-medium text-white/80 border border-white/20">
20
+ ๐Ÿ“„ Research Paper Explained
21
+ </div>
22
+ {formattedDate && (
23
+ <div className="inline-block bg-white/10 backdrop-blur-sm rounded-full px-4 py-1.5 text-sm font-medium text-white/80 border border-white/20">
24
+ ๐Ÿ“… Published {formattedDate}
25
+ </div>
26
+ )}
27
+ </div>
28
+
29
+ <h1 className="text-4xl md:text-6xl font-extrabold leading-tight mb-6 tracking-tight">
30
+ {title}
31
+ </h1>
32
+
33
+ <div className="flex flex-wrap gap-2 mb-8">
34
+ {authors?.map((author, i) => (
35
+ <span
36
+ key={i}
37
+ className="bg-white/10 backdrop-blur-sm rounded-full px-4 py-1.5 text-sm border border-white/10"
38
+ >
39
+ {author}
40
+ </span>
41
+ ))}
42
+ </div>
43
+
44
+ <div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20 max-w-3xl">
45
+ <div className="flex items-center gap-2 mb-3">
46
+ <span className="text-2xl">๐Ÿ’ก</span>
47
+ <span className="text-sm uppercase tracking-widest text-white/60 font-bold">
48
+ In one sentence
49
+ </span>
50
+ </div>
51
+ <p className="text-xl md:text-2xl font-medium leading-relaxed">{tldr}</p>
52
+ </div>
53
+ </div>
54
+ </header>
55
+ );
56
+ }
@@ -0,0 +1,35 @@
1
+ export default function KeyContributions({ items }) {
2
+ if (!items?.length) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="flex items-center gap-3 mb-6">
7
+ <div
8
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
9
+ style={{ backgroundColor: 'var(--color-primary)' }}
10
+ >
11
+ ๐ŸŒŸ
12
+ </div>
13
+ <h2 className="text-2xl font-bold">Key Contributions</h2>
14
+ </div>
15
+ <div className="grid md:grid-cols-2 gap-5 stagger-children">
16
+ {items.map((item, i) => (
17
+ <div
18
+ key={i}
19
+ className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 hover:shadow-lg transition-all hover:-translate-y-1 group"
20
+ >
21
+ <div className="flex items-start gap-4">
22
+ <div className="text-3xl shrink-0 group-hover:scale-110 transition-transform">
23
+ {item.emoji || '๐Ÿ’ก'}
24
+ </div>
25
+ <div>
26
+ <h3 className="font-bold text-lg mb-2">{item.title}</h3>
27
+ <p className="text-gray-600 leading-relaxed text-sm">{item.description}</p>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ ))}
32
+ </div>
33
+ </section>
34
+ );
35
+ }
@@ -0,0 +1,22 @@
1
+ export default function Limitations({ items }) {
2
+ if (!items?.length) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="flex items-center gap-3 mb-6">
7
+ <div className="w-10 h-10 rounded-xl flex items-center justify-center bg-gray-200 text-lg">
8
+ ๐Ÿ”
9
+ </div>
10
+ <h2 className="text-2xl font-bold">Limitations & Open Questions</h2>
11
+ </div>
12
+ <div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-4 stagger-children">
13
+ {items.map((item, i) => (
14
+ <div key={i} className="flex items-start gap-3">
15
+ <span className="text-xl shrink-0 mt-0.5">โš ๏ธ</span>
16
+ <p className="text-gray-700 leading-relaxed">{item}</p>
17
+ </div>
18
+ ))}
19
+ </div>
20
+ </section>
21
+ );
22
+ }
@@ -0,0 +1,42 @@
1
+ export default function Methodology({ methodology }) {
2
+ if (!methodology) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="flex items-center gap-3 mb-3">
7
+ <div
8
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
9
+ style={{ backgroundColor: 'var(--color-primary)' }}
10
+ >
11
+ ๐Ÿงช
12
+ </div>
13
+ <h2 className="text-2xl font-bold">Methodology</h2>
14
+ </div>
15
+ <p className="text-gray-600 mb-8 text-lg ml-[52px] leading-relaxed">{methodology.summary}</p>
16
+
17
+ <div className="space-y-6 stagger-children">
18
+ {methodology.steps?.map((step, i) => (
19
+ <div key={i} className="flex gap-5 items-start">
20
+ {/* Step number circle */}
21
+ <div className="shrink-0 relative timeline-dot">
22
+ <div
23
+ className="w-12 h-12 rounded-full flex items-center justify-center text-xl shadow-md"
24
+ style={{
25
+ backgroundColor: `color-mix(in srgb, var(--color-primary) ${100 - i * 12}%, white)`,
26
+ }}
27
+ >
28
+ {step.emoji || `${i + 1}`}
29
+ </div>
30
+ </div>
31
+
32
+ {/* Step content */}
33
+ <div className="bg-white rounded-2xl p-5 shadow-sm border border-gray-100 flex-1 hover:shadow-md transition-shadow">
34
+ <h3 className="font-bold text-base mb-2">{step.label}</h3>
35
+ <p className="text-gray-600 text-sm leading-relaxed">{step.detail}</p>
36
+ </div>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </section>
41
+ );
42
+ }
@@ -0,0 +1,34 @@
1
+ export default function Results({ items }) {
2
+ if (!items?.length) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="flex items-center gap-3 mb-6">
7
+ <div
8
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
9
+ style={{ backgroundColor: 'var(--color-primary)' }}
10
+ >
11
+ ๐Ÿ†
12
+ </div>
13
+ <h2 className="text-2xl font-bold">Key Findings</h2>
14
+ </div>
15
+ <div className="space-y-4 stagger-children">
16
+ {items.map((item, i) => (
17
+ <div
18
+ key={i}
19
+ className="bg-white rounded-2xl p-6 shadow-sm border-l-4 hover:shadow-md transition-shadow"
20
+ style={{ borderLeftColor: 'var(--color-accent)' }}
21
+ >
22
+ <div className="flex items-start gap-3">
23
+ <span className="text-2xl shrink-0">{item.emoji || 'โœจ'}</span>
24
+ <div>
25
+ <h3 className="font-bold text-lg mb-2">{item.finding}</h3>
26
+ <p className="text-gray-600 leading-relaxed text-sm">{item.detail}</p>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ ))}
31
+ </div>
32
+ </section>
33
+ );
34
+ }
@@ -0,0 +1,20 @@
1
+ export default function Significance({ text }) {
2
+ if (!text) return null;
3
+
4
+ return (
5
+ <section className="scale-in">
6
+ <div className="accent-bar rounded-2xl p-10 text-white relative overflow-hidden">
7
+ <div className="absolute top-4 right-6 text-7xl opacity-10">๐Ÿš€</div>
8
+ <div className="relative z-10">
9
+ <div className="flex items-center gap-3 mb-4">
10
+ <span className="text-3xl">๐ŸŒ</span>
11
+ <h2 className="text-2xl font-bold">Broader Impact</h2>
12
+ </div>
13
+ <p className="text-lg md:text-xl leading-relaxed text-white/90 max-w-3xl">
14
+ {text}
15
+ </p>
16
+ </div>
17
+ </div>
18
+ </section>
19
+ );
20
+ }
@@ -0,0 +1,34 @@
1
+ export default function Stats({ items }) {
2
+ if (!items?.length) return null;
3
+
4
+ return (
5
+ <section className="fade-in">
6
+ <div className="flex items-center gap-3 mb-6">
7
+ <div
8
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg"
9
+ style={{ backgroundColor: 'var(--color-accent)' }}
10
+ >
11
+ ๐Ÿ“Š
12
+ </div>
13
+ <h2 className="text-2xl font-bold">By The Numbers</h2>
14
+ </div>
15
+ <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
16
+ {items.map((stat, i) => (
17
+ <div
18
+ key={i}
19
+ className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 text-center hover:shadow-md transition-shadow"
20
+ >
21
+ <div
22
+ className="text-4xl md:text-5xl font-extrabold mb-2 stat-value"
23
+ style={{ color: 'var(--color-primary)' }}
24
+ >
25
+ {stat.value}
26
+ </div>
27
+ <div className="text-sm font-semibold text-gray-800 mb-2">{stat.label}</div>
28
+ <div className="text-xs text-gray-500 leading-relaxed">{stat.context}</div>
29
+ </div>
30
+ ))}
31
+ </div>
32
+ </section>
33
+ );
34
+ }