mirror of
https://github.com/harivansh-afk/Austens-Wedding-Guide.git
synced 2026-04-15 05:02:07 +00:00
improved literary analysis pgae and added compare works sub page
This commit is contained in:
parent
0134c81526
commit
5ca37d03f2
13 changed files with 3953 additions and 59 deletions
2488
package-lock.json
generated
2488
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,11 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.13.5",
|
||||||
|
"@emotion/styled": "^11.13.5",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@mui/icons-material": "^6.1.9",
|
||||||
|
"@mui/material": "^6.1.9",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||||
|
|
@ -42,9 +46,11 @@
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
|
@ -53,6 +59,7 @@
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-force-graph": "^1.44.7",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-resizable-panels": "^2.1.3",
|
"react-resizable-panels": "^2.1.3",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import BlogPost from './pages/BlogPost/BlogPost';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
import SuccessStories from './pages/SuccessStories';
|
import SuccessStories from './pages/SuccessStories';
|
||||||
import Analysis from './pages/Analysis';
|
import Analysis from './pages/Analysis';
|
||||||
|
import ComparativeAnalysis from './pages/ComparativeAnalysis';
|
||||||
|
import NetworkVisualization from './pages/NetworkVisualization';
|
||||||
|
|
||||||
// Lazy load other pages
|
// Lazy load other pages
|
||||||
const Quiz = React.lazy(() => import('./pages/Quiz'));
|
const Quiz = React.lazy(() => import('./pages/Quiz'));
|
||||||
|
|
@ -16,7 +18,7 @@ const MarketCalculator = React.lazy(() => import('./pages/MarketCalculator'));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router basename="/">
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
|
@ -30,6 +32,9 @@ function App() {
|
||||||
<Route path="/success-stories" element={<SuccessStories />} />
|
<Route path="/success-stories" element={<SuccessStories />} />
|
||||||
<Route path="/market-calculator" element={<MarketCalculator />} />
|
<Route path="/market-calculator" element={<MarketCalculator />} />
|
||||||
<Route path="/analysis" element={<Analysis />} />
|
<Route path="/analysis" element={<Analysis />} />
|
||||||
|
<Route path="/comparative" element={<ComparativeAnalysis />} />
|
||||||
|
<Route path="/network" element={<NetworkVisualization />} />
|
||||||
|
<Route path="*" element={<div>Page not found</div>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
||||||
19
src/components/CharacterDetails.jsx
Normal file
19
src/components/CharacterDetails.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// For smaller network visualizations in subpages
|
||||||
|
const subGraphConfig = {
|
||||||
|
nodeRelSize: 4,
|
||||||
|
linkDistance: 80,
|
||||||
|
d3: {
|
||||||
|
alphaDecay: 0.05, // Faster decay for smaller graphs
|
||||||
|
alphaMin: 0.001,
|
||||||
|
velocityDecay: 1
|
||||||
|
},
|
||||||
|
cooldownTicks: 30,
|
||||||
|
cooldownTime: 1500,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use static positioning for very small networks
|
||||||
|
const staticLayout = {
|
||||||
|
enableNodeDrag: false,
|
||||||
|
staticGraph: true,
|
||||||
|
nodePosition: { x: node => node.initialX, y: node => node.initialY }
|
||||||
|
};
|
||||||
132
src/components/InteractiveGraph.jsx
Normal file
132
src/components/InteractiveGraph.jsx
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { useRef, useCallback, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const InteractiveGraph = ({ graphData, onNodeSelect }) => {
|
||||||
|
const graphRef = useRef();
|
||||||
|
const [selectedNode, setSelectedNode] = useState(null);
|
||||||
|
const clickTimeoutRef = useRef(null);
|
||||||
|
const [isProcessingClick, setIsProcessingClick] = useState(false);
|
||||||
|
|
||||||
|
// Cleanup function for the timeout
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (clickTimeoutRef.current) {
|
||||||
|
clearTimeout(clickTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNodeClick = useCallback((node) => {
|
||||||
|
if (!node || isProcessingClick) return;
|
||||||
|
|
||||||
|
// Prevent multiple clicks
|
||||||
|
setIsProcessingClick(true);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (clickTimeoutRef.current) {
|
||||||
|
clearTimeout(clickTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the selected node immediately
|
||||||
|
setSelectedNode(node);
|
||||||
|
onNodeSelect(node);
|
||||||
|
|
||||||
|
// Reset the processing flag after a short delay
|
||||||
|
clickTimeoutRef.current = setTimeout(() => {
|
||||||
|
setIsProcessingClick(false);
|
||||||
|
}, 300); // 300ms debounce
|
||||||
|
}, [onNodeSelect, isProcessingClick]);
|
||||||
|
|
||||||
|
// Calculate static positions based on container size
|
||||||
|
const containerWidth = 600;
|
||||||
|
const containerHeight = 600;
|
||||||
|
|
||||||
|
const graphConfig = {
|
||||||
|
nodeRelSize: 8,
|
||||||
|
nodeVal: 30,
|
||||||
|
width: containerWidth,
|
||||||
|
height: containerHeight,
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
nodeColor: node => node.type === 'protagonist' ? "#4CAF50" :
|
||||||
|
node.type === 'antagonist' ? "#f44336" : "#2196F3",
|
||||||
|
|
||||||
|
// Static layout configuration
|
||||||
|
staticGraph: true,
|
||||||
|
nodePosition: node => ({
|
||||||
|
x: containerWidth/2 + Math.cos(node.index * (2 * Math.PI / graphData.nodes.length)) * (containerWidth/3),
|
||||||
|
y: containerHeight/2 + Math.sin(node.index * (2 * Math.PI / graphData.nodes.length)) * (containerHeight/3)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Interaction settings
|
||||||
|
enableNodeDrag: false,
|
||||||
|
enableZoom: true,
|
||||||
|
minZoom: 0.5,
|
||||||
|
maxZoom: 2.5,
|
||||||
|
cooldownTime: 0, // Disable force simulation cooldown
|
||||||
|
warmupTicks: 0, // Disable initial ticks
|
||||||
|
|
||||||
|
// Node appearance
|
||||||
|
nodeResolution: 16,
|
||||||
|
|
||||||
|
// Completely disable all tooltips/labels
|
||||||
|
nodeLabel: null,
|
||||||
|
nodeCanvasObject: null,
|
||||||
|
nodeCanvasObjectMode: null,
|
||||||
|
enableNodeHover: false,
|
||||||
|
enableLinkHover: false,
|
||||||
|
|
||||||
|
// Link appearance
|
||||||
|
linkWidth: 2,
|
||||||
|
linkColor: () => "#999999",
|
||||||
|
linkOpacity: 0.6,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '20px',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '1000px',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: containerWidth,
|
||||||
|
height: containerHeight,
|
||||||
|
border: '1px solid #eee',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<ForceGraph
|
||||||
|
{...graphConfig}
|
||||||
|
ref={graphRef}
|
||||||
|
graphData={graphData}
|
||||||
|
onNodeClick={handleNodeClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side popup */}
|
||||||
|
<div style={{
|
||||||
|
width: '300px',
|
||||||
|
padding: '20px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #eee',
|
||||||
|
borderRadius: '4px',
|
||||||
|
height: 'fit-content',
|
||||||
|
visibility: selectedNode ? 'visible' : 'hidden',
|
||||||
|
opacity: selectedNode ? 1 : 0,
|
||||||
|
transition: 'opacity 0.2s ease-in-out'
|
||||||
|
}}>
|
||||||
|
{selectedNode && (
|
||||||
|
<>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0' }}>{selectedNode.name}</h3>
|
||||||
|
<p style={{ margin: '0 0 10px 0' }}>Type: {selectedNode.type}</p>
|
||||||
|
<p style={{ margin: '0 0 10px 0' }}>Novel: {selectedNode.novel}</p>
|
||||||
|
{selectedNode.class && (
|
||||||
|
<p style={{ margin: '0 0 10px 0' }}>Class: {selectedNode.class}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InteractiveGraph;
|
||||||
|
|
@ -30,12 +30,20 @@ const Navbar = () => {
|
||||||
<Link to="/market-calculator" className="text-sage-700 hover:text-sage-900 transition-colors">
|
<Link to="/market-calculator" className="text-sage-700 hover:text-sage-900 transition-colors">
|
||||||
Market Value
|
Market Value
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<div className="flex items-center space-x-2">
|
||||||
to="/analysis"
|
<Link
|
||||||
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
|
to="/analysis"
|
||||||
>
|
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
|
||||||
Literary Analysis
|
>
|
||||||
</Link>
|
Literary Analysis
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/network"
|
||||||
|
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Character Network
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,19 @@ import Navbar from '../Navbar';
|
||||||
|
|
||||||
const MainLayout = ({ children }: { children: React.ReactNode }) => {
|
const MainLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-cream-50">
|
<div className="min-h-screen bg-cream-50 flex flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="flex-1 flex flex-col">
|
||||||
{children}
|
<div className="flex-1 container mx-auto px-4 sm:px-6 lg:px-8 py-8 flex flex-col">
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="bg-sage-100 border-t border-sage-200 mt-12">
|
<footer className="bg-sage-100 border-t border-sage-200">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="text-center font-cormorant text-sage-900">
|
<div className="text-center font-cormorant text-sage-900">
|
||||||
<p className="text-lg">"It is a truth universally acknowledged..."</p>
|
<p className="text-lg">"It is a truth universally acknowledged..."</p>
|
||||||
<p className="mt-2">© {new Date().getFullYear()} Austen's Wedding Guide</p>
|
<p className="mt-2">© {new Date().getFullYear()} Austen's Wedding Guide</p>
|
||||||
|
|
|
||||||
461
src/data/character-network.ts
Normal file
461
src/data/character-network.ts
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
import { CharacterNetwork } from '../types/character-network';
|
||||||
|
|
||||||
|
export const characterNetwork: CharacterNetwork = {
|
||||||
|
books: [
|
||||||
|
{
|
||||||
|
id: "pride-and-prejudice",
|
||||||
|
name: "Pride and Prejudice",
|
||||||
|
type: "book",
|
||||||
|
color: "#e91e63",
|
||||||
|
description: "A masterpiece of wit and social commentary exploring marriage, class, and prejudice in Regency England.",
|
||||||
|
year: 1813
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sense-and-sensibility",
|
||||||
|
name: "Sense and Sensibility",
|
||||||
|
type: "book",
|
||||||
|
color: "#2196f3",
|
||||||
|
description: "A story of two sisters with contrasting temperaments navigating love and heartbreak.",
|
||||||
|
year: 1811
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "northanger-abbey",
|
||||||
|
name: "Northanger Abbey",
|
||||||
|
type: "book",
|
||||||
|
color: "#4caf50",
|
||||||
|
description: "A coming-of-age story that parodies Gothic novels while exploring the perils of confusing fiction with reality.",
|
||||||
|
year: 1818
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mansfield-park",
|
||||||
|
name: "Mansfield Park",
|
||||||
|
type: "book",
|
||||||
|
color: "#9c27b0",
|
||||||
|
description: "A complex exploration of morality, social position, and personal integrity through the story of Fanny Price.",
|
||||||
|
year: 1814
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "longbourn",
|
||||||
|
name: "Longbourn",
|
||||||
|
type: "book",
|
||||||
|
color: "#795548",
|
||||||
|
description: "A retelling of Pride and Prejudice from the servants' perspective, exploring class and social inequality.",
|
||||||
|
year: 2013
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nodes: [
|
||||||
|
// Pride and Prejudice
|
||||||
|
{
|
||||||
|
id: "elizabeth-bennet",
|
||||||
|
name: "Elizabeth Bennet",
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
class: "middle",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "The witty and independent second daughter of the Bennet family, known for her intelligence and prejudiced first impressions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "darcy",
|
||||||
|
name: "Fitzwilliam Darcy",
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
class: "upper",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "A wealthy and proud nobleman who learns to overcome his prejudices and rigid social expectations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "jane-bennet",
|
||||||
|
name: "Jane Bennet",
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
class: "middle",
|
||||||
|
type: "supporting",
|
||||||
|
description: "The beautiful and gentle eldest Bennet daughter, whose sweet nature leads her to see only the good in others."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bingley",
|
||||||
|
name: "Charles Bingley",
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
class: "upper",
|
||||||
|
type: "supporting",
|
||||||
|
description: "Darcy's wealthy and amiable friend who falls in love with Jane Bennet, easily influenced by others."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lydia-bennet",
|
||||||
|
name: "Lydia Bennet",
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
class: "middle",
|
||||||
|
type: "supporting",
|
||||||
|
description: "The youngest Bennet sister, whose reckless behavior threatens her family's reputation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "wickham",
|
||||||
|
name: "George Wickham",
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
class: "middle",
|
||||||
|
type: "antagonist",
|
||||||
|
description: "A charming but deceitful officer who causes trouble for both the Bennets and Darcy."
|
||||||
|
},
|
||||||
|
// Sense and Sensibility
|
||||||
|
{
|
||||||
|
id: "elinor-dashwood",
|
||||||
|
name: "Elinor Dashwood",
|
||||||
|
novel: "Sense and Sensibility",
|
||||||
|
class: "middle",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "The sensible and reserved elder Dashwood sister, who represents sense and emotional restraint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marianne-dashwood",
|
||||||
|
name: "Marianne Dashwood",
|
||||||
|
novel: "Sense and Sensibility",
|
||||||
|
class: "middle",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "The emotional and romantic younger Dashwood sister, who learns to balance sensibility with sense."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "edward-ferrars",
|
||||||
|
name: "Edward Ferrars",
|
||||||
|
novel: "Sense and Sensibility",
|
||||||
|
class: "upper",
|
||||||
|
type: "supporting",
|
||||||
|
description: "A gentle, reserved man who forms an attachment to Elinor despite his secret engagement."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "willoughby",
|
||||||
|
name: "John Willoughby",
|
||||||
|
novel: "Sense and Sensibility",
|
||||||
|
class: "upper",
|
||||||
|
type: "antagonist",
|
||||||
|
description: "A dashing young man who captures Marianne's heart but proves morally bankrupt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "colonel-brandon",
|
||||||
|
name: "Colonel Brandon",
|
||||||
|
novel: "Sense and Sensibility",
|
||||||
|
class: "upper",
|
||||||
|
type: "supporting",
|
||||||
|
description: "A mature and honorable man who quietly loves Marianne and proves his worth through actions."
|
||||||
|
},
|
||||||
|
// Northanger Abbey
|
||||||
|
{
|
||||||
|
id: "catherine-morland",
|
||||||
|
name: "Catherine Morland",
|
||||||
|
novel: "Northanger Abbey",
|
||||||
|
class: "middle",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "A naive but charming young woman whose love of Gothic novels colors her view of reality."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "henry-tilney",
|
||||||
|
name: "Henry Tilney",
|
||||||
|
novel: "Northanger Abbey",
|
||||||
|
class: "upper",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "An intelligent and witty clergyman who guides Catherine's maturation while falling in love with her."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "isabella-thorpe",
|
||||||
|
name: "Isabella Thorpe",
|
||||||
|
novel: "Northanger Abbey",
|
||||||
|
class: "middle",
|
||||||
|
type: "antagonist",
|
||||||
|
description: "A manipulative friend who pretends affection for Catherine's brother while pursuing others."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "john-thorpe",
|
||||||
|
name: "John Thorpe",
|
||||||
|
novel: "Northanger Abbey",
|
||||||
|
class: "middle",
|
||||||
|
type: "antagonist",
|
||||||
|
description: "Isabella's boastful and dishonest brother who pursues Catherine for her perceived wealth."
|
||||||
|
},
|
||||||
|
// Mansfield Park
|
||||||
|
{
|
||||||
|
id: "fanny-price",
|
||||||
|
name: "Fanny Price",
|
||||||
|
novel: "Mansfield Park",
|
||||||
|
class: "lower",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "A modest and moral young woman taken in by her wealthy relatives, steadfast in her principles."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "edmund-bertram",
|
||||||
|
name: "Edmund Bertram",
|
||||||
|
novel: "Mansfield Park",
|
||||||
|
class: "upper",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "Fanny's cousin and friend, a would-be clergyman who is temporarily led astray by Mary Crawford."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mary-crawford",
|
||||||
|
name: "Mary Crawford",
|
||||||
|
novel: "Mansfield Park",
|
||||||
|
class: "upper",
|
||||||
|
type: "antagonist",
|
||||||
|
description: "A charming but worldly woman who attracts Edmund while representing modern urban values."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "henry-crawford",
|
||||||
|
name: "Henry Crawford",
|
||||||
|
novel: "Mansfield Park",
|
||||||
|
class: "upper",
|
||||||
|
type: "antagonist",
|
||||||
|
description: "Mary's brother, a charismatic man who pursues Fanny after toying with her cousins' affections."
|
||||||
|
},
|
||||||
|
// Longbourn
|
||||||
|
{
|
||||||
|
id: "sarah",
|
||||||
|
name: "Sarah",
|
||||||
|
novel: "Longbourn",
|
||||||
|
class: "lower",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "A hardworking housemaid at Longbourn with dreams beyond her station."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "james-smith",
|
||||||
|
name: "James Smith",
|
||||||
|
novel: "Longbourn",
|
||||||
|
class: "lower",
|
||||||
|
type: "protagonist",
|
||||||
|
description: "The mysterious new footman with a hidden past, who catches Sarah's attention."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mrs-hill",
|
||||||
|
name: "Mrs. Hill",
|
||||||
|
novel: "Longbourn",
|
||||||
|
class: "lower",
|
||||||
|
type: "supporting",
|
||||||
|
description: "The housekeeper of Longbourn, who holds many secrets about the household."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "polly",
|
||||||
|
name: "Polly",
|
||||||
|
novel: "Longbourn",
|
||||||
|
class: "lower",
|
||||||
|
type: "supporting",
|
||||||
|
description: "The young housemaid learning her duties alongside Sarah."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
relationships: [
|
||||||
|
// Pride and Prejudice Relationships
|
||||||
|
{
|
||||||
|
source: "elizabeth-bennet",
|
||||||
|
target: "jane-bennet",
|
||||||
|
type: "family",
|
||||||
|
description: "Sisters and closest confidantes",
|
||||||
|
development: [
|
||||||
|
"Share sisterly bond throughout the novel",
|
||||||
|
"Support each other through romantic trials",
|
||||||
|
"Maintain trust and confidence in each other"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "elizabeth-bennet",
|
||||||
|
target: "darcy",
|
||||||
|
type: "romance",
|
||||||
|
description: "Central romance overcoming pride and prejudice",
|
||||||
|
development: [
|
||||||
|
"Initial mutual dislike",
|
||||||
|
"Growing understanding through letters and visits",
|
||||||
|
"Eventual recognition of true character",
|
||||||
|
"Marriage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "jane-bennet",
|
||||||
|
target: "bingley",
|
||||||
|
type: "romance",
|
||||||
|
description: "Love at first sight couple",
|
||||||
|
development: [
|
||||||
|
"Immediate mutual attraction",
|
||||||
|
"Separation due to social interference",
|
||||||
|
"Reunion and marriage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "lydia-bennet",
|
||||||
|
target: "wickham",
|
||||||
|
type: "romance",
|
||||||
|
description: "Scandalous elopement",
|
||||||
|
development: [
|
||||||
|
"Flirtation and infatuation",
|
||||||
|
"Reckless elopement",
|
||||||
|
"Forced marriage through Darcy's intervention"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Sense and Sensibility Relationships
|
||||||
|
{
|
||||||
|
source: "elinor-dashwood",
|
||||||
|
target: "marianne-dashwood",
|
||||||
|
type: "family",
|
||||||
|
description: "Sisters representing sense and sensibility",
|
||||||
|
development: [
|
||||||
|
"Contrasting approaches to emotion",
|
||||||
|
"Support through family difficulties",
|
||||||
|
"Mutual growth and understanding"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "elinor-dashwood",
|
||||||
|
target: "edward-ferrars",
|
||||||
|
type: "romance",
|
||||||
|
description: "Reserved romance complicated by duty",
|
||||||
|
development: [
|
||||||
|
"Quiet mutual attraction",
|
||||||
|
"Separation due to prior engagement",
|
||||||
|
"Eventual reunion and marriage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "marianne-dashwood",
|
||||||
|
target: "willoughby",
|
||||||
|
type: "romance",
|
||||||
|
description: "Passionate but doomed romance",
|
||||||
|
development: [
|
||||||
|
"Intense initial attraction",
|
||||||
|
"Shared romantic sensibilities",
|
||||||
|
"Heartbreaking separation",
|
||||||
|
"Eventual disillusionment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "marianne-dashwood",
|
||||||
|
target: "colonel-brandon",
|
||||||
|
type: "romance",
|
||||||
|
description: "Mature love developing from friendship",
|
||||||
|
development: [
|
||||||
|
"Initial indifference from Marianne",
|
||||||
|
"Growing appreciation through trials",
|
||||||
|
"Marriage based on deeper understanding"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Northanger Abbey Relationships
|
||||||
|
{
|
||||||
|
source: "catherine-morland",
|
||||||
|
target: "henry-tilney",
|
||||||
|
type: "romance",
|
||||||
|
description: "Educational romance",
|
||||||
|
development: [
|
||||||
|
"Initial friendship and guidance",
|
||||||
|
"Growing romantic attachment",
|
||||||
|
"Overcoming misunderstandings",
|
||||||
|
"Marriage despite family opposition"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "catherine-morland",
|
||||||
|
target: "isabella-thorpe",
|
||||||
|
type: "friendship",
|
||||||
|
description: "False friendship",
|
||||||
|
development: [
|
||||||
|
"Initial close friendship",
|
||||||
|
"Growing awareness of Isabella's true nature",
|
||||||
|
"Betrayal and disillusionment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Mansfield Park Relationships
|
||||||
|
{
|
||||||
|
source: "fanny-price",
|
||||||
|
target: "edmund-bertram",
|
||||||
|
type: "romance",
|
||||||
|
description: "Constant love tested by rival",
|
||||||
|
development: [
|
||||||
|
"Childhood friendship",
|
||||||
|
"Fanny's secret love",
|
||||||
|
"Edmund's infatuation with Mary",
|
||||||
|
"Final recognition of true feelings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "edmund-bertram",
|
||||||
|
target: "mary-crawford",
|
||||||
|
type: "romance",
|
||||||
|
description: "Tempting but unsuitable attachment",
|
||||||
|
development: [
|
||||||
|
"Initial attraction",
|
||||||
|
"Growing awareness of moral differences",
|
||||||
|
"Final disillusionment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "fanny-price",
|
||||||
|
target: "henry-crawford",
|
||||||
|
type: "rivalry",
|
||||||
|
description: "Unwanted pursuit",
|
||||||
|
development: [
|
||||||
|
"Henry's initial indifference",
|
||||||
|
"Determined pursuit of Fanny",
|
||||||
|
"Fanny's steadfast refusal",
|
||||||
|
"Henry's moral failure"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Longbourn Relationships
|
||||||
|
{
|
||||||
|
source: "sarah",
|
||||||
|
target: "james-smith",
|
||||||
|
type: "romance",
|
||||||
|
description: "A romance that crosses social boundaries",
|
||||||
|
development: [
|
||||||
|
"Initial curiosity about the mysterious new footman",
|
||||||
|
"Growing understanding of each other's struggles",
|
||||||
|
"Shared moments during daily tasks",
|
||||||
|
"Overcoming past secrets and social barriers"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "sarah",
|
||||||
|
target: "mrs-hill",
|
||||||
|
type: "family",
|
||||||
|
description: "Mentor-like relationship",
|
||||||
|
development: [
|
||||||
|
"Mrs. Hill's guidance in household duties",
|
||||||
|
"Growing trust and confidence",
|
||||||
|
"Sharing of household secrets",
|
||||||
|
"Maternal care and protection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "sarah",
|
||||||
|
target: "polly",
|
||||||
|
type: "friendship",
|
||||||
|
description: "Fellow servants and friends",
|
||||||
|
development: [
|
||||||
|
"Teaching Polly household duties",
|
||||||
|
"Supporting each other through daily challenges",
|
||||||
|
"Sharing dreams and aspirations"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Add connections between Longbourn and Pride and Prejudice characters
|
||||||
|
{
|
||||||
|
source: "sarah",
|
||||||
|
target: "elizabeth-bennet",
|
||||||
|
type: "friendship",
|
||||||
|
description: "Servant and mistress with mutual respect",
|
||||||
|
development: [
|
||||||
|
"Sarah's observations of Elizabeth's character",
|
||||||
|
"Growing understanding across social boundaries",
|
||||||
|
"Shared experiences from different perspectives"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "mrs-hill",
|
||||||
|
target: "elizabeth-bennet",
|
||||||
|
type: "family",
|
||||||
|
description: "Housekeeper's care for the Bennet family",
|
||||||
|
development: [
|
||||||
|
"Years of service to the Bennet family",
|
||||||
|
"Protection of family secrets",
|
||||||
|
"Maternal guidance from below stairs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "james-smith",
|
||||||
|
target: "elizabeth-bennet",
|
||||||
|
type: "friendship",
|
||||||
|
description: "Distant but respectful relationship",
|
||||||
|
development: [
|
||||||
|
"Initial wariness",
|
||||||
|
"Growing trust through service",
|
||||||
|
"Understanding of social positions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
163
src/data/comparative-analysis.ts
Normal file
163
src/data/comparative-analysis.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
interface ThemeComparison {
|
||||||
|
theme: string;
|
||||||
|
description: string;
|
||||||
|
appearances: {
|
||||||
|
novel: string;
|
||||||
|
manifestation: string;
|
||||||
|
examples: {
|
||||||
|
quote: string;
|
||||||
|
context: string;
|
||||||
|
analysis: string;
|
||||||
|
}[];
|
||||||
|
significance: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CharacterType {
|
||||||
|
archetype: string;
|
||||||
|
description: string;
|
||||||
|
examples: {
|
||||||
|
novel: string;
|
||||||
|
character: string;
|
||||||
|
analysis: string;
|
||||||
|
evolution: string;
|
||||||
|
keyQuotes: {
|
||||||
|
quote: string;
|
||||||
|
context: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WritingStyleElement {
|
||||||
|
technique: string;
|
||||||
|
description: string;
|
||||||
|
evolution: {
|
||||||
|
novel: string;
|
||||||
|
usage: string;
|
||||||
|
examples: string[];
|
||||||
|
significance: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeComparisons: ThemeComparison[] = [
|
||||||
|
{
|
||||||
|
theme: "Marriage and Economic Reality",
|
||||||
|
description: "The intersection of marriage, economic necessity, and social mobility across Austen's works",
|
||||||
|
appearances: [
|
||||||
|
{
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
manifestation: "Marriage market and social climbing",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
quote: "It is a truth universally acknowledged...",
|
||||||
|
context: "Opening line",
|
||||||
|
analysis: "Establishes the economic premise of marriage in Regency society"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
significance: "Direct examination of marriage as economic transaction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
novel: "Sense and Sensibility",
|
||||||
|
manifestation: "Financial vulnerability of women",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
quote: "What have wealth or grandeur to do with happiness?",
|
||||||
|
context: "Marianne's naive view",
|
||||||
|
analysis: "Contrasts romantic ideals with economic reality"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
significance: "Explores the harsh realities of women's financial dependence"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
theme: "Social Class and Mobility",
|
||||||
|
description: "The examination of class boundaries and social movement in Regency society",
|
||||||
|
appearances: [
|
||||||
|
{
|
||||||
|
novel: "Mansfield Park",
|
||||||
|
manifestation: "Class consciousness and moral worth",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
quote: "We have all a better guide in ourselves...",
|
||||||
|
context: "Fanny's moral stance",
|
||||||
|
analysis: "Links class position with moral character"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
significance: "Explores the relationship between social position and moral integrity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
novel: "Northanger Abbey",
|
||||||
|
manifestation: "Social climbing and authenticity",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
quote: "No one who had ever seen Catherine Morland in her infancy...",
|
||||||
|
context: "Opening description",
|
||||||
|
analysis: "Subverts expectations of class and heroism"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
significance: "Questions the relationship between social status and personal worth"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const characterTypes: CharacterType[] = [
|
||||||
|
{
|
||||||
|
archetype: "The Witty Heroine",
|
||||||
|
description: "Intelligent, spirited female protagonists who challenge social norms",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
novel: "Pride and Prejudice",
|
||||||
|
character: "Elizabeth Bennet",
|
||||||
|
analysis: "Combines wit with social observation",
|
||||||
|
evolution: "Learns to balance judgment with understanding",
|
||||||
|
keyQuotes: [
|
||||||
|
{
|
||||||
|
quote: "I could easily forgive his pride, if he had not mortified mine",
|
||||||
|
context: "Early judgment of Darcy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
novel: "Emma",
|
||||||
|
character: "Emma Woodhouse",
|
||||||
|
analysis: "Uses wit but must learn its proper application",
|
||||||
|
evolution: "Develops from clever manipulation to genuine understanding",
|
||||||
|
keyQuotes: [
|
||||||
|
{
|
||||||
|
quote: "I seem to have been doomed to blindness",
|
||||||
|
context: "Moment of self-realization"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const writingStyleEvolution: WritingStyleElement[] = [
|
||||||
|
{
|
||||||
|
technique: "Free Indirect Discourse",
|
||||||
|
description: "Narrative technique blending character and narrator perspectives",
|
||||||
|
evolution: [
|
||||||
|
{
|
||||||
|
novel: "Northanger Abbey",
|
||||||
|
usage: "Early experimentation with narrative voice",
|
||||||
|
examples: [
|
||||||
|
"Commentary on Gothic conventions",
|
||||||
|
"Catherine's naive perspectives"
|
||||||
|
],
|
||||||
|
significance: "Develops ironic distance while maintaining character sympathy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
novel: "Emma",
|
||||||
|
usage: "Sophisticated deployment for character insight",
|
||||||
|
examples: [
|
||||||
|
"Emma's self-deceptions",
|
||||||
|
"Social observations"
|
||||||
|
],
|
||||||
|
significance: "Achieves complex character psychology and social commentary"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { novelAnalyses } from '../data/literary-analysis';
|
import { novelAnalyses } from '../data/literary-analysis';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import type { NovelAnalysis, ThematicElement, CharacterAnalysis, SocialCommentary, LiteraryDevice } from '../data/literary-analysis';
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
|
|
@ -21,9 +23,13 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "../components/ui/select";
|
} from "../components/ui/select";
|
||||||
|
import { Button } from "../components/ui/button";
|
||||||
|
|
||||||
|
type NovelKey = keyof typeof novelAnalyses;
|
||||||
|
|
||||||
const Analysis = () => {
|
const Analysis = () => {
|
||||||
const [selectedNovel, setSelectedNovel] = useState('prideAndPrejudice');
|
const [selectedNovel, setSelectedNovel] = useState<NovelKey>('prideAndPrejudice');
|
||||||
|
const navigate = useNavigate();
|
||||||
const analysis = novelAnalyses[selectedNovel];
|
const analysis = novelAnalyses[selectedNovel];
|
||||||
|
|
||||||
if (!analysis) {
|
if (!analysis) {
|
||||||
|
|
@ -40,10 +46,10 @@ const Analysis = () => {
|
||||||
<h1 className="font-cormorant text-4xl text-sage-900">
|
<h1 className="font-cormorant text-4xl text-sage-900">
|
||||||
Literary Analysis
|
Literary Analysis
|
||||||
</h1>
|
</h1>
|
||||||
<div className="max-w-xs mx-auto">
|
<div className="max-w-xs mx-auto flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={selectedNovel}
|
value={selectedNovel}
|
||||||
onValueChange={setSelectedNovel}
|
onValueChange={(value: NovelKey) => setSelectedNovel(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select a novel" />
|
<SelectValue placeholder="Select a novel" />
|
||||||
|
|
@ -55,6 +61,13 @@ const Analysis = () => {
|
||||||
<SelectItem value="mansfieldPark">Mansfield Park (1814)</SelectItem>
|
<SelectItem value="mansfieldPark">Mansfield Park (1814)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onClick={() => navigate('/comparative')}
|
||||||
|
>
|
||||||
|
Compare Works
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h2 className="font-cormorant text-2xl text-sage-900">{analysis.title}</h2>
|
<h2 className="font-cormorant text-2xl text-sage-900">{analysis.title}</h2>
|
||||||
|
|
@ -73,7 +86,7 @@ const Analysis = () => {
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="themes" className="space-y-4">
|
<TabsContent value="themes" className="space-y-4">
|
||||||
{analysis.mainThemes.map((theme, index) => (
|
{analysis.mainThemes.map((theme: ThematicElement, index: number) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{theme.theme}</CardTitle>
|
<CardTitle>{theme.theme}</CardTitle>
|
||||||
|
|
@ -82,7 +95,7 @@ const Analysis = () => {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ScrollArea className="h-[300px] rounded-md border p-4">
|
<ScrollArea className="h-[300px] rounded-md border p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{theme.examples.map((example, i) => (
|
{theme.examples.map((example, i: number) => (
|
||||||
<div key={i} className="space-y-2">
|
<div key={i} className="space-y-2">
|
||||||
<blockquote className="border-l-2 border-sage-300 pl-4 italic">
|
<blockquote className="border-l-2 border-sage-300 pl-4 italic">
|
||||||
"{example.quote}"
|
"{example.quote}"
|
||||||
|
|
@ -105,7 +118,7 @@ const Analysis = () => {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="characters" className="space-y-4">
|
<TabsContent value="characters" className="space-y-4">
|
||||||
{analysis.characterAnalysis.map((character, index) => (
|
{analysis.characterAnalysis.map((character: CharacterAnalysis, index: number) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{character.character}</CardTitle>
|
<CardTitle>{character.character}</CardTitle>
|
||||||
|
|
@ -124,7 +137,7 @@ const Analysis = () => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-sage-900">Key Quotes</h4>
|
<h4 className="font-semibold text-sage-900">Key Quotes</h4>
|
||||||
{character.keyQuotes.map((quote, i) => (
|
{character.keyQuotes.map((quote, i: number) => (
|
||||||
<div key={i} className="mt-2 space-y-2">
|
<div key={i} className="mt-2 space-y-2">
|
||||||
<blockquote className="border-l-2 border-sage-300 pl-4 italic">
|
<blockquote className="border-l-2 border-sage-300 pl-4 italic">
|
||||||
"{quote.quote}"
|
"{quote.quote}"
|
||||||
|
|
@ -144,7 +157,7 @@ const Analysis = () => {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="social" className="space-y-4">
|
<TabsContent value="social" className="space-y-4">
|
||||||
{analysis.socialCommentary.map((topic, index) => (
|
{analysis.socialCommentary.map((topic: SocialCommentary, index: number) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{topic.topic}</CardTitle>
|
<CardTitle>{topic.topic}</CardTitle>
|
||||||
|
|
@ -163,7 +176,7 @@ const Analysis = () => {
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-sage-900">Examples</h4>
|
<h4 className="font-semibold text-sage-900">Examples</h4>
|
||||||
<ul className="list-disc list-inside text-sage-700">
|
<ul className="list-disc list-inside text-sage-700">
|
||||||
{topic.examples.map((example, i) => (
|
{topic.examples.map((example: string, i: number) => (
|
||||||
<li key={i}>{example}</li>
|
<li key={i}>{example}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -176,7 +189,7 @@ const Analysis = () => {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="literary" className="space-y-4">
|
<TabsContent value="literary" className="space-y-4">
|
||||||
{analysis.literaryDevices.map((device, index) => (
|
{analysis.literaryDevices.map((device: LiteraryDevice, index: number) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{device.device}</CardTitle>
|
<CardTitle>{device.device}</CardTitle>
|
||||||
|
|
@ -191,7 +204,7 @@ const Analysis = () => {
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-sage-900">Examples</h4>
|
<h4 className="font-semibold text-sage-900">Examples</h4>
|
||||||
<ul className="list-disc list-inside text-sage-700">
|
<ul className="list-disc list-inside text-sage-700">
|
||||||
{device.examples.map((example, i) => (
|
{device.examples.map((example: string, i: number) => (
|
||||||
<li key={i}>{example}</li>
|
<li key={i}>{example}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
174
src/pages/ComparativeAnalysis.tsx
Normal file
174
src/pages/ComparativeAnalysis.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { themeComparisons, characterTypes, writingStyleEvolution } from '../data/comparative-analysis';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "../components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../components/ui/card";
|
||||||
|
import { ScrollArea } from "../components/ui/scroll-area";
|
||||||
|
import { Button } from "../components/ui/button";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
const ComparativeAnalysis = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="absolute top-24 left-8">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-sage-700 hover:text-sage-900 hover:bg-sage-100"
|
||||||
|
onClick={() => navigate('/analysis')}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Back to Analysis
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container mx-auto py-8 space-y-8">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<h1 className="font-cormorant text-4xl text-sage-900">
|
||||||
|
Comparative Analysis
|
||||||
|
</h1>
|
||||||
|
<p className="text-sage-600 max-w-2xl mx-auto">
|
||||||
|
Explore themes, character types, and writing techniques across Jane Austen's works
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="themes" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="themes">Themes</TabsTrigger>
|
||||||
|
<TabsTrigger value="characters">Character Types</TabsTrigger>
|
||||||
|
<TabsTrigger value="style">Writing Style</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="themes" className="space-y-6">
|
||||||
|
{themeComparisons.map((theme, index) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{theme.theme}</CardTitle>
|
||||||
|
<CardDescription>{theme.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{theme.appearances.map((appearance, i) => (
|
||||||
|
<div key={i} className="border-l-2 border-sage-300 pl-4">
|
||||||
|
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
|
||||||
|
{appearance.novel}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sage-700 mb-3">
|
||||||
|
{appearance.manifestation}
|
||||||
|
</p>
|
||||||
|
{appearance.examples.map((example, j) => (
|
||||||
|
<div key={j} className="mb-4">
|
||||||
|
<blockquote className="italic text-sage-800 mb-2">
|
||||||
|
"{example.quote}"
|
||||||
|
<footer className="text-sm text-sage-600">
|
||||||
|
- {example.context}
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
<p className="text-sage-700">{example.analysis}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<p className="text-sage-800 mt-2">
|
||||||
|
<span className="font-semibold">Significance: </span>
|
||||||
|
{appearance.significance}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="characters" className="space-y-6">
|
||||||
|
{characterTypes.map((type, index) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{type.archetype}</CardTitle>
|
||||||
|
<CardDescription>{type.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{type.examples.map((example, i) => (
|
||||||
|
<div key={i} className="border-l-2 border-sage-300 pl-4">
|
||||||
|
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
|
||||||
|
{example.character} in {example.novel}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sage-700 mb-3">{example.analysis}</p>
|
||||||
|
<p className="text-sage-700 mb-3">
|
||||||
|
<span className="font-semibold">Character Evolution: </span>
|
||||||
|
{example.evolution}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{example.keyQuotes.map((quote, j) => (
|
||||||
|
<blockquote key={j} className="italic text-sage-800">
|
||||||
|
"{quote.quote}"
|
||||||
|
<footer className="text-sm text-sage-600">
|
||||||
|
- {quote.context}
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="style" className="space-y-6">
|
||||||
|
{writingStyleEvolution.map((technique, index) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{technique.technique}</CardTitle>
|
||||||
|
<CardDescription>{technique.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{technique.evolution.map((stage, i) => (
|
||||||
|
<div key={i} className="border-l-2 border-sage-300 pl-4">
|
||||||
|
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
|
||||||
|
{stage.novel}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sage-700 mb-3">{stage.usage}</p>
|
||||||
|
<ul className="list-disc list-inside text-sage-700 mb-3">
|
||||||
|
{stage.examples.map((example, j) => (
|
||||||
|
<li key={j}>{example}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="text-sage-800">
|
||||||
|
<span className="font-semibold">Significance: </span>
|
||||||
|
{stage.significance}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComparativeAnalysis;
|
||||||
461
src/pages/NetworkVisualization.tsx
Normal file
461
src/pages/NetworkVisualization.tsx
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
|
import { characterNetwork } from '../data/character-network';
|
||||||
|
import { CharacterNode, Relationship, BookNode } from '../types/character-network';
|
||||||
|
import { Box, Typography, Paper, Grid, Card, CardContent, IconButton, Tooltip, Popper } from '@mui/material';
|
||||||
|
import { ForceGraph2D } from 'react-force-graph';
|
||||||
|
import { ArrowBack, Help } from '@mui/icons-material';
|
||||||
|
import * as d3 from 'd3';
|
||||||
|
|
||||||
|
interface NetworkNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
val: number;
|
||||||
|
color: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
novel?: string;
|
||||||
|
class?: string;
|
||||||
|
year?: number;
|
||||||
|
fx?: number;
|
||||||
|
fy?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetworkLink {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphData {
|
||||||
|
nodes: NetworkNode[];
|
||||||
|
links: NetworkLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TooltipState {
|
||||||
|
open: boolean;
|
||||||
|
content: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NetworkVisualization() {
|
||||||
|
const [selectedNode, setSelectedNode] = useState<CharacterNode | BookNode | null>(null);
|
||||||
|
const [selectedRelationships, setSelectedRelationships] = useState<Relationship[]>([]);
|
||||||
|
const [selectedBook, setSelectedBook] = useState<string | null>(null);
|
||||||
|
const [tooltip, setTooltip] = useState<TooltipState>({ open: false, content: '', x: 0, y: 0 });
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleNodeHover = useCallback((node: NetworkNode | null) => {
|
||||||
|
if (node) {
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
setTooltip({
|
||||||
|
open: true,
|
||||||
|
content: `${node.name}\n${node.description || ''}`,
|
||||||
|
x: rect.left + rect.width / 2, // Center of container
|
||||||
|
y: rect.top + 100 // Fixed position from top
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTooltip(prev => ({ ...prev, open: false }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNodeClick = useCallback((node: NetworkNode) => {
|
||||||
|
if (!selectedBook && node.type === 'book') {
|
||||||
|
setSelectedBook(node.id);
|
||||||
|
setSelectedNode(characterNetwork.books.find(b => b.id === node.id) || null);
|
||||||
|
} else if (selectedBook) {
|
||||||
|
const characterNode = characterNetwork.nodes.find(n => n.id === node.id);
|
||||||
|
if (characterNode) {
|
||||||
|
setSelectedNode(characterNode);
|
||||||
|
const relations = characterNetwork.relationships.filter(
|
||||||
|
r => r.source === node.id || r.target === node.id
|
||||||
|
);
|
||||||
|
setSelectedRelationships(relations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTooltip(prev => ({ ...prev, open: false }));
|
||||||
|
}, [selectedBook]);
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
setSelectedBook(null);
|
||||||
|
setSelectedNode(null);
|
||||||
|
setSelectedRelationships([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const centerGraph = useCallback((graphData: GraphData) => {
|
||||||
|
const containerWidth = 800;
|
||||||
|
const containerHeight = 700;
|
||||||
|
const padding = 50;
|
||||||
|
|
||||||
|
const xExtent = d3.extent(graphData.nodes, d => d.x || 0) as [number, number];
|
||||||
|
const yExtent = d3.extent(graphData.nodes, d => d.y || 0) as [number, number];
|
||||||
|
const xRange = xExtent[1] - xExtent[0] || 1;
|
||||||
|
const yRange = yExtent[1] - yExtent[0] || 1;
|
||||||
|
|
||||||
|
const scale = Math.min(
|
||||||
|
(containerWidth - 2 * padding) / xRange,
|
||||||
|
(containerHeight - 2 * padding) / yRange
|
||||||
|
);
|
||||||
|
|
||||||
|
graphData.nodes.forEach(node => {
|
||||||
|
node.x = ((node.x || 0) - (xExtent[0] + xRange / 2)) * scale + containerWidth / 2;
|
||||||
|
node.y = ((node.y || 0) - (yExtent[0] + yRange / 2)) * scale + containerHeight / 2;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getGraphData = useCallback((): GraphData => {
|
||||||
|
if (!selectedBook) {
|
||||||
|
// Book-level view with fixed positions in a circle
|
||||||
|
const data: GraphData = {
|
||||||
|
nodes: characterNetwork.books.map((book, idx) => {
|
||||||
|
const angle = (idx / characterNetwork.books.length) * 2 * Math.PI;
|
||||||
|
const radius = 150; // Reduced radius for better visibility
|
||||||
|
return {
|
||||||
|
...book,
|
||||||
|
val: 20,
|
||||||
|
// Set initial positions in a circle around center
|
||||||
|
x: Math.cos(angle) * radius + 400,
|
||||||
|
y: Math.sin(angle) * radius + 350,
|
||||||
|
// Don't fix positions to allow force layout to adjust
|
||||||
|
fx: undefined,
|
||||||
|
fy: undefined
|
||||||
|
};
|
||||||
|
}) as NetworkNode[],
|
||||||
|
links: [] as NetworkLink[]
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookName = characterNetwork.books.find(b => b.id === selectedBook)?.name;
|
||||||
|
const nodes = characterNetwork.nodes
|
||||||
|
.filter(node => node.novel === bookName)
|
||||||
|
.map((node, idx) => {
|
||||||
|
const angle = (idx / characterNetwork.nodes.length) * 2 * Math.PI;
|
||||||
|
const radius = 100; // Initial radius for character layout
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
val: 10,
|
||||||
|
// Set initial positions in a circle
|
||||||
|
x: Math.cos(angle) * radius + 400,
|
||||||
|
y: Math.sin(angle) * radius + 350,
|
||||||
|
// Clear any fixed positions
|
||||||
|
fx: undefined,
|
||||||
|
fy: undefined,
|
||||||
|
color: node.type === 'protagonist' ? '#e91e63' :
|
||||||
|
node.type === 'antagonist' ? '#f44336' : '#2196f3'
|
||||||
|
};
|
||||||
|
}) as NetworkNode[];
|
||||||
|
|
||||||
|
const data: GraphData = {
|
||||||
|
nodes,
|
||||||
|
links: characterNetwork.relationships
|
||||||
|
.filter(rel =>
|
||||||
|
nodes.some((n: NetworkNode) => n.id === rel.source) &&
|
||||||
|
nodes.some((n: NetworkNode) => n.id === rel.target)
|
||||||
|
)
|
||||||
|
.map(rel => ({
|
||||||
|
source: rel.source,
|
||||||
|
target: rel.target,
|
||||||
|
color: rel.type === 'family' ? '#4caf50' :
|
||||||
|
rel.type === 'romance' ? '#e91e63' :
|
||||||
|
rel.type === 'friendship' ? '#2196f3' : '#ff9800'
|
||||||
|
})) as NetworkLink[]
|
||||||
|
};
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [selectedBook]);
|
||||||
|
|
||||||
|
const renderNodeCanvas = useCallback((node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) => {
|
||||||
|
const size = node.val || 5;
|
||||||
|
const fontSize = Math.max(12 / scale, 2);
|
||||||
|
|
||||||
|
// Draw node
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(node.x || 0, node.y || 0, size, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = node.color;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = '#fff';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Only draw labels when zoomed in enough or for book nodes
|
||||||
|
if (scale > 0.7 || node.type === 'book') {
|
||||||
|
const labelDistance = size + fontSize;
|
||||||
|
ctx.font = `${fontSize}px Inter, Arial`;
|
||||||
|
const textWidth = ctx.measureText(node.name).width;
|
||||||
|
|
||||||
|
// Semi-transparent background for better readability
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
||||||
|
const padding = fontSize * 0.3;
|
||||||
|
const backgroundHeight = fontSize + padding * 2;
|
||||||
|
|
||||||
|
// Draw background with rounded corners
|
||||||
|
const cornerRadius = 4;
|
||||||
|
const backgroundY = (node.y || 0) + labelDistance - fontSize / 2 - padding;
|
||||||
|
roundRect(
|
||||||
|
ctx,
|
||||||
|
(node.x || 0) - textWidth / 2 - padding,
|
||||||
|
backgroundY,
|
||||||
|
textWidth + padding * 2,
|
||||||
|
backgroundHeight,
|
||||||
|
cornerRadius
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw text
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(
|
||||||
|
node.name,
|
||||||
|
node.x || 0,
|
||||||
|
(node.y || 0) + labelDistance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Helper function for drawing rounded rectangles
|
||||||
|
const roundRect = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
radius: number
|
||||||
|
) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + radius, y);
|
||||||
|
ctx.lineTo(x + width - radius, y);
|
||||||
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||||
|
ctx.lineTo(x + width, y + height - radius);
|
||||||
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||||
|
ctx.lineTo(x + radius, y + height);
|
||||||
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||||
|
ctx.lineTo(x, y + radius);
|
||||||
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLegendTooltip = () => (
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>Color Legend:</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Books:<br />
|
||||||
|
• Pride and Prejudice - Pink<br />
|
||||||
|
• Sense and Sensibility - Blue<br />
|
||||||
|
• Northanger Abbey - Green<br />
|
||||||
|
• Mansfield Park - Purple<br />
|
||||||
|
Longbourn - Brown
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Characters:<br />
|
||||||
|
• Protagonists - Pink<br />
|
||||||
|
• Antagonists - Red<br />
|
||||||
|
• Supporting - Blue
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Relationships:<br />
|
||||||
|
• Family - Green<br />
|
||||||
|
• Romance - Pink<br />
|
||||||
|
• Friendship - Blue<br />
|
||||||
|
• Rivalry - Orange
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 2,
|
||||||
|
px: 3,
|
||||||
|
pt: 2
|
||||||
|
}}>
|
||||||
|
<Typography variant="h4">
|
||||||
|
Character Network & Timeline
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title={getLegendTooltip()} arrow placement="right">
|
||||||
|
<IconButton size="small" sx={{ ml: 2 }}>
|
||||||
|
<Help />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={0} sx={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<Grid item xs={12} md={8}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
minHeight: 700,
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
{selectedBook && (
|
||||||
|
<Tooltip title="Return to book overview" arrow>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleBackClick}
|
||||||
|
sx={{ position: 'absolute', top: 8, left: 8, zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Box sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 1,
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{!selectedBook ? 'Click a book to explore its characters' : 'Click characters to view relationships'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ForceGraph2D
|
||||||
|
graphData={getGraphData()}
|
||||||
|
onNodeHover={handleNodeHover}
|
||||||
|
onNodeClick={handleNodeClick}
|
||||||
|
nodeCanvasObject={(node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) =>
|
||||||
|
renderNodeCanvas(node, ctx, scale)}
|
||||||
|
linkColor={(link: NetworkLink) => link.color}
|
||||||
|
linkWidth={2}
|
||||||
|
nodeRelSize={6}
|
||||||
|
width={800}
|
||||||
|
height={700}
|
||||||
|
cooldownTicks={50}
|
||||||
|
cooldownTime={3000}
|
||||||
|
linkDirectionalParticles={2}
|
||||||
|
linkDirectionalParticleSpeed={0.003}
|
||||||
|
d3AlphaDecay={0.1}
|
||||||
|
d3VelocityDecay={0.4}
|
||||||
|
minZoom={0.5}
|
||||||
|
maxZoom={4}
|
||||||
|
dagMode={selectedBook ? undefined : 'radialin'}
|
||||||
|
dagLevelDistance={100}
|
||||||
|
enablePanInteraction={true}
|
||||||
|
enableZoomInteraction={true}
|
||||||
|
onEngineStop={() => {
|
||||||
|
if (!selectedBook) {
|
||||||
|
centerGraph(getGraphData());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onNodeDragEnd={(node: NetworkNode) => {
|
||||||
|
if (node.x && node.y) {
|
||||||
|
node.fx = node.x;
|
||||||
|
node.fy = node.y;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
warmupTicks={100}
|
||||||
|
onZoom={() => centerGraph(getGraphData())}
|
||||||
|
centerAt={[400, 350]}
|
||||||
|
zoom={2}
|
||||||
|
enableNodeDrag={true}
|
||||||
|
enableZoomPanInteraction={true}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Box sx={{
|
||||||
|
height: '100%',
|
||||||
|
minHeight: 700,
|
||||||
|
overflow: 'auto',
|
||||||
|
borderLeft: '1px solid rgba(0, 0, 0, 0.12)'
|
||||||
|
}}>
|
||||||
|
{selectedNode && (
|
||||||
|
<Card sx={{ boxShadow: 'none', borderRadius: 0 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
{selectedNode.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="textSecondary" gutterBottom>
|
||||||
|
{selectedNode.type === 'book' ? `Published: ${(selectedNode as BookNode).year}` : selectedNode.novel}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" paragraph>
|
||||||
|
{selectedNode.description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{selectedNode.type !== 'book' && (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2" sx={{ mt: 2 }} color="textSecondary">
|
||||||
|
Social Class: {(selectedNode as CharacterNode).class}
|
||||||
|
<br />
|
||||||
|
Character Role: {selectedNode.type}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" sx={{ mt: 3, mb: 2 }}>
|
||||||
|
Relationships
|
||||||
|
</Typography>
|
||||||
|
{selectedRelationships.map((rel, index) => (
|
||||||
|
<Box key={index} sx={{ mt: 2, p: 2, bgcolor: 'rgba(0, 0, 0, 0.02)', borderRadius: 1 }}>
|
||||||
|
<Typography variant="subtitle1" color="primary">
|
||||||
|
{rel.description}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||||
|
Type: {rel.type}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Development:
|
||||||
|
</Typography>
|
||||||
|
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
|
||||||
|
{rel.development.map((step, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<Typography variant="body2">{step}</Typography>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Popper
|
||||||
|
open={tooltip.open}
|
||||||
|
anchorEl={containerRef.current}
|
||||||
|
placement="top"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${tooltip.x}px`,
|
||||||
|
top: `${tooltip.y}px`,
|
||||||
|
zIndex: 1500
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper sx={{
|
||||||
|
p: 2,
|
||||||
|
maxWidth: 300,
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
boxShadow: 3,
|
||||||
|
borderRadius: 1
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" whiteSpace="pre-line">
|
||||||
|
{tooltip.content}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Popper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/types/character-network.ts
Normal file
31
src/types/character-network.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export interface BookNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'book';
|
||||||
|
color: string;
|
||||||
|
description: string;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
novel: string;
|
||||||
|
class: string;
|
||||||
|
type: 'protagonist' | 'antagonist' | 'supporting';
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Relationship {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
type: 'family' | 'romance' | 'friendship' | 'rivalry';
|
||||||
|
description: string;
|
||||||
|
development: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterNetwork {
|
||||||
|
books: BookNode[];
|
||||||
|
nodes: CharacterNode[];
|
||||||
|
relationships: Relationship[];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue