mirror of
https://github.com/danilo-znamerovszkij/c-atlas.git
synced 2025-12-23 14:08:29 -05:00
🧑🏼💻 integrate analytics, add About button, and enhance UI elements
This commit is contained in:
0
.cursor/commands/about.md
Normal file
0
.cursor/commands/about.md
Normal file
197
README.md
197
README.md
@@ -1,42 +1,107 @@
|
||||
# Consciousness Atlas
|
||||
|
||||
Interactive chart visualization project built with Vite, TypeScript, and ECharts.
|
||||
An interactive web application that visualizes Robert Lawrence Kuhn's "Landscape of Consciousness" - mapping over 325 theories of phenomenal consciousness in an explorable sunburst chart.
|
||||
|
||||
## Features
|
||||
## Tech Stack & Architecture
|
||||
|
||||
- ⚡ **Super fast development** with Vite
|
||||
- 🎯 **TypeScript** for strong typing and better DX
|
||||
- 📊 **ECharts integration** with tree-shaking for optimal bundle size
|
||||
- 🎨 **Canvas renderer** for crisp, scalable charts
|
||||
- 🔧 **Clean imports** with vite-tsconfig-paths
|
||||
- 📱 **Responsive design** with automatic resize handling
|
||||
- **Frontend**: TypeScript + Vite with ESBuild
|
||||
- **Charts**: ECharts 6.0 with Sunburst visualization (SVG/Canvas renderers)
|
||||
- **Styling**: SCSS with responsive design and mobile optimization
|
||||
- **Deployment**: Vercel with SPA routing and file-based routing
|
||||
- **Form Submissions**: Telegram Bot API integration for feedback collection
|
||||
- **Dependencies**: Minimal - only ECharts, Vite, TypeScript, and SCSS
|
||||
|
||||
## Tech Stack
|
||||
## Technical Features
|
||||
|
||||
- **Bundler**: Vite (super fast dev + optimized build)
|
||||
- **Language**: TypeScript (strong typing for ECharts config + DX)
|
||||
- **Charts**: ECharts with tree-shaking (only import what you need)
|
||||
- **Renderer**: SVG (scales well for screenshots/exports)
|
||||
- **Build**: ESBuild for fast builds
|
||||
- 🚀 **Minimal Bundle Size** - Tree-shaking and code splitting (only SunburstChart, SVGRenderer, CanvasRenderer, TitleComponent)
|
||||
- 📱 **Responsive Design** - Dynamic label positioning and mobile-optimized interactions
|
||||
- 🎨 **Dynamic Color System** - Mystic-themed palette with automatic hierarchy-based color variations
|
||||
- 🔄 **SPA Routing** - Client-side routing with history API fallback
|
||||
- 📊 **High Performance** - SVG renderer for crisp scaling, Canvas fallback
|
||||
- 🤖 **Telegram Integration** - Form submissions sent directly to Telegram bot
|
||||
|
||||
## Form Submissions & Telegram Integration
|
||||
|
||||
The feedback form uses a serverless API endpoint (`api/submit.ts`) that forwards submissions to a Telegram bot:
|
||||
|
||||
- **API Endpoint**: `/api/submit` - Handles POST requests with form data
|
||||
- **Telegram Bot**: Sends formatted messages to a designated Telegram chat
|
||||
- **Error Handling**: Graceful fallback with user feedback
|
||||
- **Security**: Basic validation and sanitization of form inputs
|
||||
|
||||
## Analytics & Mixpanel Integration
|
||||
|
||||
The application includes comprehensive analytics tracking using Mixpanel:
|
||||
|
||||
### Setup
|
||||
1. Create a Mixpanel project and get your project token
|
||||
2. Set the environment variable: `VITE_MIXPANEL_TOKEN=your_token_here`
|
||||
3. Analytics will automatically initialize in production mode
|
||||
|
||||
### Tracked Events
|
||||
|
||||
1. **Page Views** - Each theory navigation is tracked as a page view
|
||||
- Event: `Page View`
|
||||
- Properties: `theory_name`, `category`, `subcategory`, `page_type`
|
||||
|
||||
2. **Click Tracking** - Various user interactions
|
||||
- Event: `Click`
|
||||
- Elements tracked:
|
||||
- Feedback button clicks
|
||||
- GitHub link clicks
|
||||
- Kuhn paper link clicks
|
||||
- Twitter/X link clicks
|
||||
- "Read more" link clicks
|
||||
- Properties: `element`, `position`, `link_url`, `click_type`
|
||||
|
||||
3. **Form Submissions** - Feedback form interactions
|
||||
- Event: `Form Submission`
|
||||
- Properties: `form_type`, `success`, `error_message`, `submission_type`
|
||||
|
||||
### Analytics Configuration
|
||||
- **Debug Mode**: Enabled in development, disabled in production
|
||||
- **Persistence**: Uses localStorage for session persistence
|
||||
- **Tracking**: Only tracks in production when token is provided
|
||||
- **Properties**: Includes timestamp, URL, user agent, and additional custom properties
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
├── config/ # Chart configurations
|
||||
│ └── chartConfig.ts # ECharts options and data
|
||||
├── types/ # TypeScript type definitions
|
||||
├── main.ts # Main application entry point
|
||||
└── style.css # Global styles
|
||||
├── components/ # UI components
|
||||
│ ├── TheoryChart.ts # Main sunburst chart component
|
||||
│ ├── SearchBar.ts # Theory search functionality
|
||||
│ ├── FormPopup.ts # Feedback form modal
|
||||
│ └── ItemDetailsPanel.ts # Theory detail viewer
|
||||
├── config/ # Configuration files
|
||||
│ ├── appConfig.ts # App settings and environment variables
|
||||
│ └── chartConfig.ts # Chart data, colors, and ECharts options
|
||||
├── data/ # Data files
|
||||
│ ├── theoryNames.ts # Theory name mappings
|
||||
│ └── THEORY.md # Theory documentation
|
||||
├── pages/ # Page-specific styles
|
||||
│ └── theory.scss # Theory detail page styling
|
||||
├── styles/ # Global styles
|
||||
│ ├── main.scss # Main stylesheet
|
||||
│ └── _mixins.scss # SCSS mixins
|
||||
├── types/ # TypeScript definitions
|
||||
│ ├── theory.ts # Mind Theory Taxon Schema (MTTS) interface
|
||||
│ └── chart.ts # Chart-related types
|
||||
├── utils/ # Utility functions
|
||||
│ ├── routing.ts # Client-side routing
|
||||
│ ├── globalState.ts # Application state management
|
||||
│ ├── chartUtils.ts # Chart helper functions
|
||||
│ ├── slugUtils.ts # URL slug utilities
|
||||
│ └── apiMock.ts # Mock API for development
|
||||
└── main.ts # Application entry point
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm or yarn
|
||||
- Node.js 22.x
|
||||
- npm
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -44,7 +109,7 @@ src/
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
# Start development server (runs on port 8080)
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
@@ -52,45 +117,91 @@ npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Type checking
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
The project uses:
|
||||
- **Path mapping** for clean imports (`@/components/*`, `@/config/*`)
|
||||
- **Tree-shaking** - only import the ECharts modules you need
|
||||
- **Path mapping** for clean imports (`@/components/*`, `@/config/*`, `@/types/*`)
|
||||
- **Tree-shaking** - Only imports required ECharts components (SunburstChart, SVGRenderer, CanvasRenderer)
|
||||
- **Code splitting** - ECharts is automatically chunked for better performance
|
||||
- **SCSS** - Modular styling with mixins and responsive design
|
||||
- **TypeScript** - Strict typing
|
||||
|
||||
## Chart Configuration
|
||||
|
||||
Chart options are kept in a separate config file (`src/config/chartConfig.ts`) for maintainability. You can easily:
|
||||
The sunburst chart is configured in `src/config/chartConfig.ts`:
|
||||
|
||||
- Modify chart data
|
||||
- Change chart types
|
||||
- Update styling and themes
|
||||
- Add new chart configurations
|
||||
- **Data Structure**: Hierarchical theory organization (10 main categories → subcategories → individual theories)
|
||||
- **Color Palette**: Mystic-themed colors with automatic lightening/desaturation for hierarchy levels
|
||||
- **Label Positioning**: Dynamic positioning based on device type and hierarchy level
|
||||
- **Interactive Features**: Tooltips, click handlers, and responsive behavior
|
||||
|
||||
## Deployment
|
||||
|
||||
This project can be easily deployed on:
|
||||
- **Vercel** - `npm run build` then deploy the `dist` folder
|
||||
- **Netlify** - Connect your repo and it will auto-deploy
|
||||
- **GitHub Pages** - `npm run build` then push the `dist` folder to gh-pages branch
|
||||
The project is configured for Vercel deployment:
|
||||
|
||||
```bash
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Deploy to Vercel (if using Vercel CLI)
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
The `vercel.json` configuration includes:
|
||||
- SPA routing with history API fallback
|
||||
- Asset caching headers
|
||||
- Build command configuration
|
||||
|
||||
## Performance Features
|
||||
|
||||
- **Tree-shaking**: Only loads required ECharts components
|
||||
- **SVG renderer**: Better for static/interactive charts
|
||||
- **Code splitting**: Automatic chunking of heavy dependencies
|
||||
- **Optimized builds**: ESBuild for fast production builds
|
||||
- **Tree-shaking**: Only loads required ECharts components (SunburstChart, SVGRenderer, CanvasRenderer, TitleComponent)
|
||||
- **Code splitting**: ECharts automatically chunked for better loading performance
|
||||
- **Responsive rendering**: SVG renderer for crisp scaling, Canvas fallback
|
||||
- **Optimized builds**: ESBuild for fast development and production builds
|
||||
- **Mobile optimization**: Dynamic label visibility and positioning
|
||||
|
||||
## Customization
|
||||
|
||||
To add new chart types:
|
||||
1. Import the required ECharts components in `main.ts`
|
||||
2. Add the component to `echarts.use()`
|
||||
3. Create new chart options in `chartConfig.ts`
|
||||
4. Update the chart type in the series configuration
|
||||
To modify the chart:
|
||||
|
||||
1. **Data**: Edit `baseData` in `src/config/chartConfig.ts`
|
||||
2. **Colors**: Modify `mysticPalette` and color utility functions
|
||||
3. **Styling**: Update SCSS files in `src/styles/` and `src/pages/`
|
||||
4. **Chart Options**: Modify `getChartOptions()` function
|
||||
5. **Theory Data**: Add entries to data files in `src/data/`
|
||||
|
||||
---
|
||||
|
||||
## About the Consciousness Atlas
|
||||
|
||||
The Consciousness Atlas is a free web app that transforms Kuhn's 2024 academic paper into an interactive visualization. It presents theories of consciousness organized along a spectrum from most physical (Materialism) to least physical (Idealism), allowing users to explore the field visually and access detailed theory entries.
|
||||
|
||||
## Features
|
||||
|
||||
- 🧠 **Interactive Sunburst Chart** - Visualize 325+ consciousness theories in a hierarchical layout
|
||||
- 📚 **Detailed Theory Entries** - Click any theory to read structured summaries with sources
|
||||
- 🔍 **Search Functionality** - Find specific theories quickly
|
||||
- 📱 **Responsive Design** - Optimized for desktop and mobile viewing
|
||||
|
||||
## Theory Data
|
||||
|
||||
Theories are organized using Kuhn's taxonomy with structured data following the Mind Theory Taxon Schema (MTTS):
|
||||
|
||||
- **IdAndClass**: Theory title, summary, thinkers, category classification
|
||||
- **ConceptualGround**: Ontological status, mind-body relationship, qualia accounts
|
||||
- **MechanismAndDynamics**: Scope, mechanisms, evidence, evolutionary accounts
|
||||
- **EmpiricsAndCritiques**: Testability, criticisms, limitations
|
||||
- **Implications**: Stances on AI consciousness, survival after death, meaning
|
||||
- **RelationsAndSources**: Related theories and academic references
|
||||
|
||||
## Academic Context
|
||||
|
||||
Based on Robert Lawrence Kuhn's 2024 paper "A Landscape of Consciousness" published in Progress in Biophysics and Molecular Biology. The Atlas follows Kuhn's "collect and categorize, not assess and adjudicate" approach, providing a neutral visualization of the consciousness research landscape.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node'
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
// Only allow POST requests
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' })
|
||||
}
|
||||
@@ -9,23 +8,22 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
try {
|
||||
const { name, email, message } = req.body
|
||||
|
||||
// Validate required fields
|
||||
if (!message || !message.trim()) {
|
||||
return res.status(400).json({ error: 'Message is required' })
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
const tgToken = process.env.TG_BOT_TOKEN
|
||||
const chatId = process.env.TG_CHAT_ID
|
||||
|
||||
// If Telegram is configured, send message
|
||||
const randomEmojis = ['🧠', '💭', '🤔', '💡', '💥', '🔍', '🔦', '🔬', '🔭', '🔮', '🔥', '🔒', '🔖', '🔗']
|
||||
const randomIndex = Math.floor(Math.random() * randomEmojis.length)
|
||||
const randomEmoji = randomEmojis[randomIndex]
|
||||
|
||||
if (tgToken && chatId) {
|
||||
const telegramMessage = `📨 New form submission from C-Atlas
|
||||
${name ? `Name: ${name}` : 'Name: Not provided'}
|
||||
${email ? `Email: ${email}` : 'Email: Not provided'}
|
||||
Message: ${message}
|
||||
|
||||
Timestamp: ${new Date().toISOString()}`
|
||||
const telegramMessage = `
|
||||
${randomEmoji} 📨 ${message}
|
||||
${name ? `From: ${name}` : ''}${email ? `(${email})` : ''}
|
||||
`
|
||||
|
||||
const telegramResponse = await fetch(`https://api.telegram.org/bot${tgToken}/sendMessage`, {
|
||||
method: 'POST',
|
||||
@@ -44,7 +42,6 @@ Timestamp: ${new Date().toISOString()}`
|
||||
return res.status(500).json({ error: 'Failed to send notification' })
|
||||
}
|
||||
} else {
|
||||
// Log to console if Telegram is not configured
|
||||
console.log('Form submission received:', {
|
||||
name: name || 'Not provided',
|
||||
email: email || 'Not provided',
|
||||
@@ -53,7 +50,6 @@ Timestamp: ${new Date().toISOString()}`
|
||||
})
|
||||
}
|
||||
|
||||
// Return success response
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Form submitted successfully'
|
||||
|
||||
31
index.html
31
index.html
@@ -350,7 +350,7 @@
|
||||
</div>
|
||||
|
||||
<div class="logo-container">
|
||||
<svg class="logo">
|
||||
<svg viewBox="0 0 247 40" class="logo">
|
||||
<use xlink:href="#logo"></use>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -394,6 +394,24 @@
|
||||
</svg>
|
||||
Feedback
|
||||
</button>
|
||||
|
||||
<button id="about-button" class="about-btn">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 152 148"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M62.2493 0.482929C48.9547 2.29901 37.1824 7.02968 27.1809 14.5745C14.043 24.4853 5.43586 39.6905 2.32413 58.4866C1.31249 64.5969 0.963481 69.2453 0.966821 76.5513C0.98519 114.648 15.4303 137.129 44.6673 144.564C52.8396 146.643 59.7905 147.427 72.1018 147.661C82.8127 147.864 86.5203 147.539 93.7313 145.763C108.075 142.23 120.925 133.955 130.258 122.239C138.694 111.649 144.843 97.3364 148.298 80.2466L149.419 74.7036C149.419 74.7036 151.229 63.8584 151.229 57.1553C151.229 51.4167 149.419 43.4614 149.419 43.4614L148.484 40.438C147.265 36.4998 144.373 30.4586 142.023 26.9417C138.203 21.2271 131.895 15.3999 125.304 11.498C115.4 5.6352 101.073 1.62579 85.2942 0.300851C79.6452 -0.173493 66.2808 -0.0676729 62.2493 0.482929ZM69.096 10.2345C58.2441 11.1735 50.1049 13.2869 42.2102 17.2156C36.5138 20.0503 32.6746 22.8194 28.1782 27.3364C19.0247 36.5317 13.703 48.69 11.7999 64.7545C11.1413 70.3129 11.1376 82.326 11.7923 88.5432C14.6836 115.998 26.7795 130.536 50.7268 135.34C61.2163 137.444 76.8601 138.152 85.2588 136.902C116.04 132.323 133.555 110.6 139.584 69.5275C141.808 54.3744 139.865 42.1744 133.758 32.9335C125.869 20.9983 109.557 13.1847 87.7533 10.8973C82.8982 10.388 71.8553 9.99567 69.096 10.2345ZM74.9407 26.3276C73.6548 26.5712 71.9442 27.0112 71.1393 27.3062C63.5014 30.1022 58.4165 37.5671 57.84 46.8302C57.675 49.4804 57.7398 50.0374 58.3534 51.2475C59.2521 53.0192 60.8789 54.0435 62.795 54.0435C65.9789 54.0435 68.094 51.6368 68.094 48.0144C68.094 43.9573 69.9075 39.7873 72.3633 38.1969C74.58 36.7618 75.5556 36.4847 78.9485 36.3265C83.293 36.1242 85.4038 36.8404 87.5549 39.2468C92.1686 44.4084 90.124 49.2459 78.1212 61.5651C74.6812 65.0958 71.0127 69.1029 69.9693 70.4691C67.8078 73.3001 66.0283 76.8083 65.2725 79.7292C64.168 83.9967 64.9265 89.8225 67.1769 94.356C68.5279 97.0767 69.8478 98.0513 72.1833 98.0513C76.4052 98.0513 78.3427 94.321 76.3705 89.9888C75.3214 87.6839 75.0335 86.3828 75.0609 84.0679C75.103 80.5402 76.6156 78.2212 84.1106 70.193C92.7611 60.9275 96.8487 55.5438 98.6993 50.979C100.516 46.498 100.045 40.7931 97.4249 35.5273C94.2487 29.1441 88.3108 25.7955 80.2844 25.861C78.6312 25.8744 76.2265 26.0844 74.9407 26.3276ZM75.6087 103.222C74.224 103.831 72.182 106.028 71.7351 107.389C71.4365 108.299 71.4723 108.805 71.9175 109.978C72.6319 111.86 74.9901 114.061 76.6216 114.368C79.2067 114.856 82.4864 112.481 83.118 109.665C83.4864 108.021 82.9947 106.372 81.6908 104.879C79.9515 102.886 77.7405 102.284 75.6087 103.222Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span class="tooltip">About</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main class="ca-main">
|
||||
@@ -509,7 +527,7 @@
|
||||
<div class="faq-item">
|
||||
<h3>How many theories does the Atlas include?</h3>
|
||||
<p>
|
||||
The Atlas visualizes over 325 individual theories, grouped into ten
|
||||
The Atlas visualizes over 220 individual theories, grouped into ten
|
||||
broad categories.
|
||||
</p>
|
||||
</div>
|
||||
@@ -530,15 +548,6 @@
|
||||
where available.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>Can I use the Atlas data for research?</h3>
|
||||
<p>
|
||||
Yes. Cite the Atlas and the original sources. If you plan wide
|
||||
redistribution or commercial use, please contact the team for
|
||||
licensing details.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="related">
|
||||
|
||||
83
package-lock.json
generated
83
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"echarts": "^6.0.0",
|
||||
"mixpanel-browser": "^2.71.0",
|
||||
"vite": "^5.4.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
@@ -391,6 +392,52 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixpanel/rrdom": {
|
||||
"version": "2.0.0-alpha.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@mixpanel/rrdom/-/rrdom-2.0.0-alpha.18.2.tgz",
|
||||
"integrity": "sha512-vX/tbnS14ZzzatC7vOyvAm9tOLU8tof0BuppBlphzEx1YHTSw8DQiAmyAc0AmXidchLV0W+cUHV/WsehPLh2hQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mixpanel/rrweb-snapshot": "^2.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixpanel/rrweb": {
|
||||
"version": "2.0.0-alpha.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@mixpanel/rrweb/-/rrweb-2.0.0-alpha.18.2.tgz",
|
||||
"integrity": "sha512-J3dVTEu6Z4p8di7y9KKvUooNuBjX97DdG6XGWoPEPi07A9512h9M8MEtvlY3mK0PGfuC0Mz5Pv/Ws6gjGYfKQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mixpanel/rrdom": "^2.0.0-alpha.18",
|
||||
"@mixpanel/rrweb-snapshot": "^2.0.0-alpha.18",
|
||||
"@mixpanel/rrweb-types": "^2.0.0-alpha.18",
|
||||
"@mixpanel/rrweb-utils": "^2.0.0-alpha.18",
|
||||
"@types/css-font-loading-module": "0.0.7",
|
||||
"@xstate/fsm": "^1.4.0",
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
"mitt": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixpanel/rrweb-snapshot": {
|
||||
"version": "2.0.0-alpha.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@mixpanel/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.2.tgz",
|
||||
"integrity": "sha512-2kSnjZZ3QZ9zOz/isOt8s54mXUUDgXk/u0eEi/rE0xBWDeuA0NHrBcqiMc+w4F/yWWUpo5F5zcuPeYpc6ufAsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss": "^8.4.38"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixpanel/rrweb-types": {
|
||||
"version": "2.0.0-alpha.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@mixpanel/rrweb-types/-/rrweb-types-2.0.0-alpha.18.2.tgz",
|
||||
"integrity": "sha512-ucIYe1mfJ2UksvXW+d3bOySTB2/0yUSqQJlUydvbBz6OO2Bhq3nJHyLXV9ExkgUMZm1ZyDcvvmNUd1+5tAXlpA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mixpanel/rrweb-utils": {
|
||||
"version": "2.0.0-alpha.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@mixpanel/rrweb-utils/-/rrweb-utils-2.0.0-alpha.18.2.tgz",
|
||||
"integrity": "sha512-OomKIB6GTx5xvCLJ7iic2khT/t/tnCJUex13aEqsbSqIT/UzUUsqf+LTrgUK5ex+f6odmkCNjre2y5jvpNqn+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
@@ -709,12 +756,33 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/css-font-loading-module": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
|
||||
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xstate/fsm": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz",
|
||||
"integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
@@ -930,6 +998,21 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mixpanel-browser": {
|
||||
"version": "2.71.0",
|
||||
"resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.71.0.tgz",
|
||||
"integrity": "sha512-jKmDXe68/oQFgk/9ns9Z36bA0CJ31PH8Y77XTLLGfJvhsUPbvu+7Se9e281NejZF6+OMqx7cE+zFxToozYyNrA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mixpanel/rrweb": "2.0.0-alpha.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"echarts": "^6.0.0",
|
||||
"mixpanel-browser": "^2.71.0",
|
||||
"vite": "^5.4.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { EChartsOption } from 'echarts'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { TooltipComponent } from 'echarts/components'
|
||||
|
||||
// Register the tooltip component
|
||||
echarts.use([TooltipComponent])
|
||||
|
||||
export class ChartContainer {
|
||||
@@ -22,7 +21,6 @@ export class ChartContainer {
|
||||
renderer
|
||||
})
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.chart?.resize()
|
||||
})
|
||||
@@ -36,7 +34,6 @@ export class ChartContainer {
|
||||
|
||||
refreshData() {
|
||||
if (this.chart) {
|
||||
// Force a re-render by setting the option again
|
||||
this.chart.setOption(this.chart.getOption(), true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
transform: scale(0.9) translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
||||
.form-overlay.visible & {
|
||||
transform: scale(1) translateY(0);
|
||||
@@ -72,7 +73,7 @@
|
||||
|
||||
&:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@@ -108,6 +109,7 @@
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(247, 184, 1, 0.6);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mockApiSubmit, isApiAvailable } from '@/utils/apiMock'
|
||||
import analytics from '@/utils/analytics'
|
||||
|
||||
export class FormPopup {
|
||||
private container: HTMLElement
|
||||
@@ -149,6 +150,7 @@ export class FormPopup {
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
analytics.trackFormSubmission('feedback', true)
|
||||
this.showSuccess('Message sent successfully!')
|
||||
this.form.reset()
|
||||
setTimeout(() => this.hide(), 1500)
|
||||
@@ -157,6 +159,7 @@ export class FormPopup {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error)
|
||||
analytics.trackFormSubmission('feedback', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
this.showError('Failed to send message. Please try again.')
|
||||
} finally {
|
||||
submitBtn.disabled = false
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
.item-details-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -550px;
|
||||
width: 550px;
|
||||
right: -750px;
|
||||
width: 750px;
|
||||
height: 100vh;
|
||||
background: #100f0a;
|
||||
backdrop-filter: blur(20px);
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
.mystic-icon {
|
||||
font-size: 56px;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
margin-bottom: 20px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
h5 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -166,7 +166,7 @@
|
||||
.item-tags {
|
||||
h5 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -181,7 +181,7 @@
|
||||
.tag {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
@@ -236,8 +236,9 @@
|
||||
}
|
||||
|
||||
.theory-tagline {
|
||||
color: #3b82f6;
|
||||
font-size: 16px;
|
||||
color: #649cf9;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
margin-bottom: 16px;
|
||||
@@ -267,7 +268,7 @@
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -284,7 +285,7 @@
|
||||
.overview-item {
|
||||
h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -325,7 +326,7 @@
|
||||
|
||||
h3 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -400,8 +401,8 @@
|
||||
color: #ffffff;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 0;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,7 +410,7 @@
|
||||
.theory-components {
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -423,7 +424,7 @@
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -476,7 +477,7 @@
|
||||
.implication-question {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { baseData } from '../config/chartConfig'
|
||||
import type { TheoryData } from '../types/theory'
|
||||
import analytics from '../utils/analytics'
|
||||
|
||||
interface ItemData {
|
||||
name: string
|
||||
@@ -32,7 +33,6 @@ export class ItemDetailsPanel {
|
||||
}
|
||||
|
||||
private initializeItemData() {
|
||||
// Sample data for some key items - you can expand this
|
||||
this.itemData.set('Eliminative', {
|
||||
name: 'Eliminative',
|
||||
heading: 'Eliminative Materialism',
|
||||
@@ -72,8 +72,6 @@ export class ItemDetailsPanel {
|
||||
category: 'Quantum',
|
||||
subcategory: 'Quantum Extensions'
|
||||
})
|
||||
|
||||
// Add more items as needed...
|
||||
}
|
||||
|
||||
private render() {
|
||||
@@ -105,11 +103,9 @@ export class ItemDetailsPanel {
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
this.hide()
|
||||
// Call the close callback to update URL
|
||||
this.onCloseCallback?.()
|
||||
})
|
||||
|
||||
// Add keyboard support
|
||||
closeBtn.addEventListener('keydown', (e) => {
|
||||
const keyboardEvent = e as KeyboardEvent
|
||||
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
|
||||
@@ -120,7 +116,6 @@ export class ItemDetailsPanel {
|
||||
})
|
||||
}
|
||||
|
||||
// Add escape key support
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const keyboardEvent = e as KeyboardEvent
|
||||
if (keyboardEvent.key === 'Escape' && this.isVisible) {
|
||||
@@ -139,33 +134,27 @@ export class ItemDetailsPanel {
|
||||
const itemData = this.itemData.get(itemName)
|
||||
|
||||
if (itemData) {
|
||||
// Update the content with modern scholarly layout
|
||||
const infoElement = this.container.querySelector('#item-info')
|
||||
if (infoElement) {
|
||||
infoElement.innerHTML = `
|
||||
<div class="scholarly-panel">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb">
|
||||
${this.buildBreadcrumb(itemName).split(' > ').map((item, index, array) =>
|
||||
`<span class="breadcrumb-item">${item}</span>${index < array.length - 1 ? '<span class="breadcrumb-separator">›</span>' : ''}`
|
||||
).join('')}
|
||||
</div>
|
||||
|
||||
<!-- Headline -->
|
||||
<h1 class="headline">${itemData.heading}</h1>
|
||||
|
||||
<!-- Thinker Line -->
|
||||
<div class="thinker-line">
|
||||
<span class="thinker-name">${itemName}</span>
|
||||
<span class="thinker-role">Philosopher & Theorist</span>
|
||||
</div>
|
||||
|
||||
<!-- Quote Block -->
|
||||
<blockquote class="summary-quote">
|
||||
<p>${itemData.text}</p>
|
||||
</blockquote>
|
||||
|
||||
<!-- Shields Section -->
|
||||
<div class="shields-section">
|
||||
<div class="shield" data-tooltip="Core philosophical position">
|
||||
<div class="shield-icon">⚖️</div>
|
||||
@@ -185,7 +174,6 @@ export class ItemDetailsPanel {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CORE ONTOLOGY Section -->
|
||||
<div class="callout">
|
||||
<div class="callout-title">CORE ONTOLOGY</div>
|
||||
<div class="callout-table">
|
||||
@@ -200,7 +188,6 @@ export class ItemDetailsPanel {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CRITIQUE & RELATED THEORIES Section -->
|
||||
<div class="relations-block">
|
||||
<h3 class="relations-title">CRITIQUE & RELATED THEORIES</h3>
|
||||
<div class="relations-grid">
|
||||
@@ -211,7 +198,6 @@ export class ItemDetailsPanel {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IMPLICATIONS Section -->
|
||||
<div class="faq-section">
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">IMPLICATIONS</div>
|
||||
@@ -235,7 +221,6 @@ export class ItemDetailsPanel {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button class="action-button">Phenomenal</button>
|
||||
<button class="action-button">Access <span class="separator">·</span> Memory</button>
|
||||
@@ -244,7 +229,6 @@ export class ItemDetailsPanel {
|
||||
`
|
||||
}
|
||||
} else {
|
||||
// Fallback for items without data
|
||||
const infoElement = this.container.querySelector('#item-info')
|
||||
if (infoElement) {
|
||||
infoElement.innerHTML = `
|
||||
@@ -322,8 +306,8 @@ export class ItemDetailsPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Attach FAQ event listeners
|
||||
this.attachFAQListeners()
|
||||
this.attachLinkTracking()
|
||||
}
|
||||
|
||||
private attachFAQListeners() {
|
||||
@@ -347,14 +331,25 @@ export class ItemDetailsPanel {
|
||||
})
|
||||
}
|
||||
|
||||
private attachLinkTracking() {
|
||||
const readMoreLinks = this.container.querySelectorAll('a[href="#"]')
|
||||
readMoreLinks.forEach(link => {
|
||||
link.addEventListener('click', (event) => {
|
||||
const rect = link.getBoundingClientRect()
|
||||
analytics.trackClick('read_more_link', {
|
||||
x: (event as MouseEvent).clientX - rect.left,
|
||||
y: (event as MouseEvent).clientY - rect.top
|
||||
}, link.getAttribute('href') || '')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private buildBreadcrumb(itemName: string): string {
|
||||
// Search for the item in the imported baseData hierarchy
|
||||
for (const topLevel of baseData) {
|
||||
for (const secondLevel of topLevel.children) {
|
||||
if (secondLevel.name === itemName) {
|
||||
return `${topLevel.name}`
|
||||
}
|
||||
// Check if it's a third-level item
|
||||
if ('children' in secondLevel && secondLevel.children) {
|
||||
for (const thirdLevel of secondLevel.children) {
|
||||
if (thirdLevel.name === itemName) {
|
||||
@@ -365,7 +360,6 @@ export class ItemDetailsPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for items not found in the hierarchy
|
||||
return 'Philosophy'
|
||||
}
|
||||
|
||||
@@ -375,13 +369,11 @@ export class ItemDetailsPanel {
|
||||
panel?.classList.add('visible')
|
||||
panel?.setAttribute('aria-hidden', 'false')
|
||||
|
||||
// Update the title
|
||||
const titleElement = this.container.querySelector('#item-title')
|
||||
if (titleElement) {
|
||||
titleElement.textContent = theoryData.id_and_class.theory_title
|
||||
}
|
||||
|
||||
// Update the content with theory data using MTTS v5.0 format
|
||||
const infoElement = this.container.querySelector('#item-info')
|
||||
if (infoElement) {
|
||||
infoElement.innerHTML = `
|
||||
@@ -646,6 +638,8 @@ export class ItemDetailsPanel {
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
this.attachLinkTracking()
|
||||
}
|
||||
|
||||
public showError(message: string) {
|
||||
@@ -654,13 +648,11 @@ export class ItemDetailsPanel {
|
||||
panel?.classList.add('visible')
|
||||
panel?.setAttribute('aria-hidden', 'false')
|
||||
|
||||
// Update the title
|
||||
const titleElement = this.container.querySelector('#item-title')
|
||||
if (titleElement) {
|
||||
titleElement.textContent = 'Error'
|
||||
}
|
||||
|
||||
// Update the content with error message
|
||||
const infoElement = this.container.querySelector('#item-info')
|
||||
if (infoElement) {
|
||||
infoElement.innerHTML = `
|
||||
@@ -679,22 +671,18 @@ export class ItemDetailsPanel {
|
||||
panel?.classList.add('visible')
|
||||
panel?.setAttribute('aria-hidden', 'false')
|
||||
|
||||
// Convert theory slug to display name, preserving dashes
|
||||
const theoryName = theory
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('-')
|
||||
|
||||
// Convert category slug to display name
|
||||
const categoryName = category.charAt(0).toUpperCase() + category.slice(1)
|
||||
|
||||
// Update the title
|
||||
const titleElement = this.container.querySelector('#item-title')
|
||||
if (titleElement) {
|
||||
titleElement.textContent = theoryName
|
||||
}
|
||||
|
||||
// Update the content with loading message and breadcrumb
|
||||
const infoElement = this.container.querySelector('#item-info')
|
||||
if (infoElement) {
|
||||
infoElement.innerHTML = `
|
||||
|
||||
@@ -154,19 +154,16 @@ export class SearchBar {
|
||||
this.dropdown.classList.add('visible')
|
||||
this.input.setAttribute('aria-expanded', 'true')
|
||||
|
||||
// Add click handlers to dropdown items
|
||||
this.dropdown.querySelectorAll('.search-item').forEach((item, index) => {
|
||||
item.addEventListener('click', () => {
|
||||
const theoryName = item.getAttribute('data-theory')
|
||||
if (theoryName) {
|
||||
// Try to load the theory directly and let the router handle category detection
|
||||
this.loadAndNavigateToTheory(theoryName)
|
||||
this.input.value = ''
|
||||
this.hideDropdown()
|
||||
}
|
||||
})
|
||||
|
||||
// Add keyboard navigation
|
||||
item.addEventListener('keydown', (e) => {
|
||||
const keyboardEvent = e as KeyboardEvent
|
||||
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getTheoryFullName } from '@/data/theoryNames'
|
||||
import { Router } from '@/utils/routing'
|
||||
import { generateSlug } from '@/utils/slugUtils'
|
||||
import globalState from '@/utils/globalState'
|
||||
import analytics from '@/utils/analytics'
|
||||
|
||||
export class TheoryChart {
|
||||
private chartContainer: ChartContainer
|
||||
@@ -65,7 +66,6 @@ export class TheoryChart {
|
||||
if (params.data && params.data.name) {
|
||||
this.handleTheoryClick(params.data)
|
||||
} else {
|
||||
// Clicked outside chart - hide mobile labels
|
||||
if (this.isMobile()) {
|
||||
setMobileLabelVisibility(false)
|
||||
this.refreshChartWithNewData()
|
||||
@@ -81,7 +81,6 @@ export class TheoryChart {
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout)
|
||||
resizeTimeout = window.setTimeout(() => {
|
||||
// Reset mobile label visibility on resize
|
||||
setMobileLabelVisibility(false)
|
||||
this.chartContainer.setOption(getChartOptions())
|
||||
}, 100)
|
||||
@@ -142,7 +141,6 @@ export class TheoryChart {
|
||||
const chart = this.chartContainer.getChart()
|
||||
if (!chart) return
|
||||
|
||||
// Dispatch a highlight event to the chart
|
||||
chart.dispatchAction({
|
||||
type: 'highlight',
|
||||
seriesIndex: 0,
|
||||
@@ -179,16 +177,23 @@ export class TheoryChart {
|
||||
|
||||
private handleTheoryClick(theoryData: any) {
|
||||
if (theoryData.children) {
|
||||
// Parent category clicked - show mobile labels if on mobile
|
||||
if (this.isMobile()) {
|
||||
if (this.isMobile() && theoryData.name !== 'Materialism') {
|
||||
setMobileLabelVisibility(true)
|
||||
this.refreshChartWithNewData()
|
||||
}
|
||||
|
||||
const chart = this.chartContainer.getChart()
|
||||
if (chart) {
|
||||
chart.dispatchAction({
|
||||
type: 'sunburstRootToNode',
|
||||
targetNodeId: theoryData.name
|
||||
})
|
||||
}
|
||||
|
||||
this.itemDetailsPanel.hide()
|
||||
return
|
||||
}
|
||||
|
||||
// Leaf node clicked - hide mobile labels if on mobile
|
||||
if (this.isMobile()) {
|
||||
setMobileLabelVisibility(false)
|
||||
this.refreshChartWithNewData()
|
||||
@@ -202,14 +207,22 @@ export class TheoryChart {
|
||||
if (response.ok) {
|
||||
const slug = generateSlug(theoryName)
|
||||
const category = globalState.getTheoryCategory(theoryName) || theoryData.parent?.toLowerCase() || 'neurobiological'
|
||||
|
||||
// Track page view for theory navigation
|
||||
analytics.trackPageView(theoryName, category, theoryData.parent)
|
||||
|
||||
this.router.navigateToTheory(category, slug)
|
||||
} else {
|
||||
// No JSON data, show generic item details
|
||||
// Track page view for generic theory display
|
||||
const category = theoryData.parent?.toLowerCase() || 'unknown'
|
||||
analytics.trackPageView(theoryName, category, theoryData.parent)
|
||||
this.itemDetailsPanel.show(theoryData.name)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Error loading, show generic item details
|
||||
// Track page view for error case
|
||||
const category = theoryData.parent?.toLowerCase() || 'unknown'
|
||||
analytics.trackPageView(theoryName, category, theoryData.parent)
|
||||
this.itemDetailsPanel.show(theoryData.name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { getTheoryFullName } from '@/data/theoryNames'
|
||||
|
||||
// Color palette - Mystic Depths & Illumination
|
||||
const mysticPalette = {
|
||||
mainColors: [
|
||||
'#03045E', // Midnight Blue
|
||||
@@ -17,7 +16,6 @@ const mysticPalette = {
|
||||
]
|
||||
}
|
||||
|
||||
// Color utility functions
|
||||
export const colorUtils = {
|
||||
// Lighten a color by percentage
|
||||
lighten: (hex: string, percent: number): string => {
|
||||
@@ -31,7 +29,6 @@ export const colorUtils = {
|
||||
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)
|
||||
},
|
||||
|
||||
// Desaturate a color by percentage
|
||||
desaturate: (hex: string, percent: number): string => {
|
||||
const num = parseInt(hex.replace('#', ''), 16)
|
||||
const R = num >> 16
|
||||
@@ -45,7 +42,6 @@ export const colorUtils = {
|
||||
return '#' + (0x1000000 + newR * 0x10000 + newG * 0x100 + newB).toString(16).slice(1)
|
||||
},
|
||||
|
||||
// Adjust saturation down by percentage
|
||||
reduceSaturation: (hex: string, percent: number): string => {
|
||||
const num = parseInt(hex.replace('#', ''), 16)
|
||||
const R = num >> 16
|
||||
@@ -66,75 +62,62 @@ export const colorUtils = {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get current palette colors
|
||||
export const getCurrentPalette = () => mysticPalette
|
||||
|
||||
// Global state for mobile label visibility
|
||||
let showMobileLabels = false
|
||||
|
||||
// Function to set mobile label visibility
|
||||
export const setMobileLabelVisibility = (visible: boolean) => {
|
||||
showMobileLabels = visible
|
||||
}
|
||||
|
||||
// Function to get mobile label visibility
|
||||
export const getMobileLabelVisibility = () => showMobileLabels
|
||||
|
||||
// Function to apply colors to data
|
||||
export const applyPaletteToData = (data: any[]) => {
|
||||
let mainColorIndex = 0
|
||||
|
||||
const applyColors = (items: any[], level: number = 0, parentColor?: string, parentName?: string) => {
|
||||
console.log(`Applying colors to items: ${items.length} at level ${level}`)
|
||||
return items.map((item, index) => {
|
||||
let color: string
|
||||
let labelPosition: string | undefined
|
||||
let showLabel = true
|
||||
|
||||
console.log(`Processing item: ${item.name} at level ${level}`)
|
||||
|
||||
if (level === 0) {
|
||||
// Top level - assign unique color from palette
|
||||
color = mysticPalette.mainColors[mainColorIndex % mysticPalette.mainColors.length]
|
||||
mainColorIndex++
|
||||
// Top level labels stay inside
|
||||
labelPosition = undefined
|
||||
} else if (level === 1 && parentColor) {
|
||||
// Second level - lighten by 20-30% with variation based on index
|
||||
const lightenAmount = 20 + (index * 3) % 15 // 20-35% variation
|
||||
const lightenAmount = 20 + (index * 3) % 15
|
||||
color = colorUtils.lighten(parentColor, lightenAmount)
|
||||
|
||||
// For Materialism category, subcategories stay inside
|
||||
// For other categories, subcategories go outside
|
||||
if (parentName === 'Materialism') {
|
||||
labelPosition = undefined // Stay inside
|
||||
labelPosition = undefined
|
||||
} else {
|
||||
if (isMobile() && !showMobileLabels) {
|
||||
showLabel = false;
|
||||
// labelPosition = undefined;
|
||||
} else if (isMobile() && showMobileLabels) {
|
||||
labelPosition = undefined
|
||||
} else {
|
||||
labelPosition = 'outside'
|
||||
}
|
||||
labelPosition = 'outside' // Go outside
|
||||
}
|
||||
} else if (level >= 2 && parentColor) {
|
||||
// Third level and beyond - desaturate with variation based on index
|
||||
const desatAmount = 25 + (index * 2) % 20 // 25-45% variation
|
||||
const desatAmount = 25 + (index * 2) % 20
|
||||
color = colorUtils.desaturate(parentColor, desatAmount)
|
||||
|
||||
// Third level and beyond always go outside
|
||||
labelPosition = 'outside'
|
||||
|
||||
// On mobile, hide labels for the last row (level 4+ - outermost ring)
|
||||
if (isMobile() && !showMobileLabels) {
|
||||
showLabel = false;
|
||||
// labelPosition = undefined;
|
||||
} else if (isMobile() && showMobileLabels) {
|
||||
labelPosition = undefined
|
||||
} else {
|
||||
labelPosition = 'outside'
|
||||
}
|
||||
} else {
|
||||
color = '#666666' // Fallback color
|
||||
color = '#666666'
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
...item,
|
||||
parent: parentName, // Add parent name to each item
|
||||
parent: parentName,
|
||||
itemStyle: { color },
|
||||
label: showLabel && labelPosition ? {
|
||||
position: labelPosition,
|
||||
@@ -155,7 +138,6 @@ export const applyPaletteToData = (data: any[]) => {
|
||||
return applyColors(data)
|
||||
}
|
||||
|
||||
// Base data structure without colors
|
||||
export const baseData = [
|
||||
{
|
||||
name: 'Materialism',
|
||||
@@ -468,28 +450,23 @@ export const baseData = [
|
||||
}
|
||||
]
|
||||
|
||||
// Get current data with applied palette
|
||||
export const getCurrentData = () => {
|
||||
return applyPaletteToData(baseData)
|
||||
}
|
||||
|
||||
// Function to refresh chart with current mobile label settings
|
||||
export const refreshChartData = () => {
|
||||
return applyPaletteToData(baseData)
|
||||
}
|
||||
|
||||
// Function to check if device is mobile
|
||||
const isMobile = () => {
|
||||
const isMobileDevice = window.innerWidth <= 768
|
||||
return isMobileDevice
|
||||
}
|
||||
|
||||
// Function to get chart options with current mobile state
|
||||
export const getChartOptions = (): EChartsOption => {
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
title: {
|
||||
// text: 'C-Atlas Chart',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
@@ -498,7 +475,6 @@ export const getChartOptions = (): EChartsOption => {
|
||||
tooltip: {
|
||||
show: true,
|
||||
formatter: function (params: any) {
|
||||
// Don't show tooltip if no parent or if parent is Materialism
|
||||
if (!params.data || params.data.parent === undefined || params.data.parent === 'Materialism') {
|
||||
return ''
|
||||
}
|
||||
@@ -524,21 +500,15 @@ export const getChartOptions = (): EChartsOption => {
|
||||
label: {
|
||||
rotate: 'radial',
|
||||
show: true,
|
||||
// fontSize: 18,
|
||||
formatter: function (params: any) {
|
||||
// console.log(params.data.parent)
|
||||
// Make Materialism and Monism titles bigger
|
||||
if (params.data.parent === undefined) {
|
||||
return `{title|${params.name}}`
|
||||
}
|
||||
return params.name
|
||||
},
|
||||
rich: {
|
||||
// fontSize: 18,
|
||||
title: {
|
||||
fontSize: 14,
|
||||
// fontWeight: 'bold',
|
||||
// color: '#FCD771'
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -588,17 +558,14 @@ export const getChartOptions = (): EChartsOption => {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to extract all theory names from chart data in order
|
||||
export const getAllTheoryNames = (): string[] => {
|
||||
const theoryNames: string[] = []
|
||||
|
||||
const extractTheories = (items: any[]) => {
|
||||
items.forEach(item => {
|
||||
// Only add items that don't have children (leaf nodes - actual theories)
|
||||
if (!item.children) {
|
||||
theoryNames.push(item.name)
|
||||
} else {
|
||||
// Recursively process children
|
||||
extractTheories(item.children)
|
||||
}
|
||||
})
|
||||
@@ -608,6 +575,5 @@ export const getAllTheoryNames = (): string[] => {
|
||||
return theoryNames
|
||||
}
|
||||
|
||||
// Legacy chart options (kept for backward compatibility)
|
||||
export const chartOptions: EChartsOption = getChartOptions()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Static mapping for all theories
|
||||
const staticTheoryNames: Record<string, string> = {
|
||||
// 1.1 Philosophical Theories
|
||||
'Eliminative': 'Eliminative Materialism / Illusionism',
|
||||
|
||||
93
src/main.ts
93
src/main.ts
@@ -8,6 +8,7 @@ import { SearchBar } from '@/components/SearchBar'
|
||||
import { FormPopup } from '@/components/FormPopup'
|
||||
import { Router } from '@/utils/routing'
|
||||
import globalState from '@/utils/globalState'
|
||||
import analytics from '@/utils/analytics'
|
||||
|
||||
import './components/ItemDetailsPanel.scss'
|
||||
import './components/FormPopup.scss'
|
||||
@@ -30,7 +31,51 @@ new SearchBar('search-container', router)
|
||||
|
||||
const feedbackButton = document.getElementById('feedback-button')
|
||||
if (feedbackButton) {
|
||||
feedbackButton.addEventListener('click', () => formPopup.show())
|
||||
feedbackButton.addEventListener('click', (event) => {
|
||||
const rect = feedbackButton.getBoundingClientRect()
|
||||
analytics.trackClick('feedback_button', {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
})
|
||||
formPopup.show()
|
||||
})
|
||||
}
|
||||
|
||||
const aboutButton = document.getElementById('about-button')
|
||||
if (aboutButton) {
|
||||
aboutButton.addEventListener('click', () => {
|
||||
window.scrollBy({
|
||||
top: window.innerHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
|
||||
// Handle scroll behavior - fade out when scrolling down
|
||||
let lastScrollY = window.scrollY
|
||||
let ticking = false
|
||||
|
||||
const updateAboutButton = () => {
|
||||
const currentScrollY = window.scrollY
|
||||
const scrollThreshold = 100 // Start fading after 100px scroll
|
||||
|
||||
if (currentScrollY > scrollThreshold) {
|
||||
aboutButton.classList.add('fade-out')
|
||||
} else {
|
||||
aboutButton.classList.remove('fade-out')
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY
|
||||
ticking = false
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(updateAboutButton)
|
||||
ticking = true
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
}
|
||||
|
||||
const logoContainer = document.querySelector('.logo-container')
|
||||
@@ -44,6 +89,52 @@ if (logoContainer) {
|
||||
|
||||
itemDetailsPanel.setCloseCallback(() => router.goHome())
|
||||
|
||||
// Add click tracking for external links
|
||||
const addLinkTracking = () => {
|
||||
// GitHub link
|
||||
const githubLink = document.querySelector('.github-link')
|
||||
if (githubLink) {
|
||||
githubLink.addEventListener('click', (event) => {
|
||||
const rect = githubLink.getBoundingClientRect()
|
||||
analytics.trackClick('github_link', {
|
||||
x: (event as MouseEvent).clientX - rect.left,
|
||||
y: (event as MouseEvent).clientY - rect.top
|
||||
}, 'https://github.com')
|
||||
})
|
||||
}
|
||||
|
||||
// Kuhn paper links
|
||||
const kuhnLinks = document.querySelectorAll('a[href*="sciencedirect.com"]')
|
||||
kuhnLinks.forEach(link => {
|
||||
link.addEventListener('click', (event) => {
|
||||
const rect = link.getBoundingClientRect()
|
||||
analytics.trackClick('kuhn_paper_link', {
|
||||
x: (event as MouseEvent).clientX - rect.left,
|
||||
y: (event as MouseEvent).clientY - rect.top
|
||||
}, link.getAttribute('href') || '')
|
||||
})
|
||||
})
|
||||
|
||||
// Twitter/X link
|
||||
const twitterLink = document.querySelector('a[href*="x.com"]')
|
||||
if (twitterLink) {
|
||||
twitterLink.addEventListener('click', (event) => {
|
||||
const rect = twitterLink.getBoundingClientRect()
|
||||
analytics.trackClick('twitter_link', {
|
||||
x: (event as MouseEvent).clientX - rect.left,
|
||||
y: (event as MouseEvent).clientY - rect.top
|
||||
}, twitterLink.getAttribute('href') || '')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize link tracking after DOM is loaded
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', addLinkTracking)
|
||||
} else {
|
||||
addLinkTracking()
|
||||
}
|
||||
|
||||
router.setLoadingCallback((category, theory) => {
|
||||
itemDetailsPanel.showLoading(category, theory)
|
||||
})
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1b1b1b;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
@@ -91,7 +95,7 @@
|
||||
button {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
color: #649cf9;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
@@ -174,6 +178,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
.about-btn {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(247, 184, 1, 0.1);
|
||||
border: 1px solid rgba(247, 184, 1, 0.5);
|
||||
color: #f7b801;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
&:hover {
|
||||
background: rgba(247, 184, 1, 0.15);
|
||||
border-color: rgba(247, 184, 1, 0.7);
|
||||
transform: translateY(-1px) scale(1.05);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid rgba(247, 184, 1, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #ffffff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(247, 184, 1, 0.3);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
}
|
||||
|
||||
&.fade-out {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
#theory-search {
|
||||
width: 100%;
|
||||
padding: 12px 16px 12px 40px;
|
||||
@@ -263,20 +347,25 @@
|
||||
|
||||
.logo-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.logo {
|
||||
width: 120px;
|
||||
height: 19px;
|
||||
height: 27px;
|
||||
padding: 6px 10px;
|
||||
margin: 0;
|
||||
backdrop-filter: blur(6px);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
top: 70px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
width: auto;
|
||||
@@ -284,7 +373,7 @@
|
||||
}
|
||||
|
||||
.github-link {
|
||||
top: 16px;
|
||||
top: 20px;
|
||||
right: 16px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -303,20 +392,33 @@
|
||||
width: auto;
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
.about-btn {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.logo-container {
|
||||
top: 12px;
|
||||
top: 16px;
|
||||
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 16px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
top: 40px;
|
||||
top: 60px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
@@ -329,6 +431,18 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.about-btn {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
#theory-search {
|
||||
padding: 10px 12px 10px 36px;
|
||||
font-size: 13px;
|
||||
@@ -341,7 +455,7 @@
|
||||
}
|
||||
|
||||
.github-link {
|
||||
top: 12px;
|
||||
top: 16px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
103
src/utils/analytics.ts
Normal file
103
src/utils/analytics.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import mixpanel from 'mixpanel-browser'
|
||||
|
||||
interface AnalyticsConfig {
|
||||
token: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
class Analytics {
|
||||
private mixpanel: typeof mixpanel | null = null
|
||||
private config: AnalyticsConfig
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
token: import.meta.env.VITE_MIXPANEL_TOKEN || '',
|
||||
enabled: !!import.meta.env.VITE_MIXPANEL_TOKEN && import.meta.env.PROD
|
||||
}
|
||||
|
||||
if (this.config.enabled) {
|
||||
this.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
try {
|
||||
mixpanel.init(this.config.token, {
|
||||
debug: !import.meta.env.PROD,
|
||||
track_pageview: false, // We'll handle page views manually
|
||||
persistence: 'localStorage'
|
||||
})
|
||||
this.mixpanel = mixpanel
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Mixpanel:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private track(event: string, properties?: Record<string, any>) {
|
||||
if (!this.mixpanel || !this.config.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.mixpanel.track(event, {
|
||||
...properties,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
user_agent: navigator.userAgent
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to track event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Page view tracking for theory navigation
|
||||
trackPageView(theoryName: string, category: string, subcategory?: string) {
|
||||
this.track('Page View', {
|
||||
theory_name: theoryName,
|
||||
category: category,
|
||||
subcategory: subcategory,
|
||||
page_type: 'theory'
|
||||
})
|
||||
}
|
||||
|
||||
// Click tracking for various elements
|
||||
trackClick(element: string, position?: { x: number; y: number }, linkUrl?: string) {
|
||||
this.track('Click', {
|
||||
element: element,
|
||||
position: position,
|
||||
link_url: linkUrl,
|
||||
click_type: 'interaction'
|
||||
})
|
||||
}
|
||||
|
||||
// Form submission tracking
|
||||
trackFormSubmission(formType: string, success: boolean, errorMessage?: string) {
|
||||
this.track('Form Submission', {
|
||||
form_type: formType,
|
||||
success: success,
|
||||
error_message: errorMessage,
|
||||
submission_type: 'feedback'
|
||||
})
|
||||
}
|
||||
|
||||
// Identify user (optional, for future use)
|
||||
identify(userId: string, properties?: Record<string, any>) {
|
||||
if (!this.mixpanel || !this.config.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.mixpanel.identify(userId)
|
||||
if (properties) {
|
||||
this.mixpanel.people.set(properties)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to identify user:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const analytics = new Analytics()
|
||||
|
||||
export default analytics
|
||||
@@ -1,9 +1,6 @@
|
||||
// Mock API for development when not using Vercel CLI
|
||||
export const mockApiSubmit = async (data: { name: string; email: string; message: string }) => {
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Log to console for development
|
||||
console.log('📨 Mock form submission:', {
|
||||
name: data.name || 'Not provided',
|
||||
email: data.email || 'Not provided',
|
||||
@@ -11,14 +8,12 @@ export const mockApiSubmit = async (data: { name: string; email: string; message
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// Simulate success response
|
||||
return {
|
||||
success: true,
|
||||
message: 'Form submitted successfully (mock)'
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're in development and API is not available
|
||||
export const isApiAvailable = () => {
|
||||
return typeof window !== 'undefined' && window.location.hostname !== 'localhost'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { ChartDataItem } from '@/types/chart'
|
||||
|
||||
export const chartUtils = {
|
||||
/**
|
||||
* Calculate the total value of a chart data tree
|
||||
*/
|
||||
calculateTotal(data: ChartDataItem[]): number {
|
||||
return data.reduce((total, item) => {
|
||||
const itemValue = item.value || 0
|
||||
@@ -12,9 +9,6 @@ export const chartUtils = {
|
||||
}, 0)
|
||||
},
|
||||
|
||||
/**
|
||||
* Flatten chart data into a single array
|
||||
*/
|
||||
flattenData(data: ChartDataItem[]): ChartDataItem[] {
|
||||
const result: ChartDataItem[] = []
|
||||
|
||||
@@ -31,9 +25,6 @@ export const chartUtils = {
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a specific item in the chart data
|
||||
*/
|
||||
findItem(data: ChartDataItem[], name: string): ChartDataItem | null {
|
||||
for (const item of data) {
|
||||
if (item.name === name) {
|
||||
@@ -47,9 +38,6 @@ export const chartUtils = {
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate random chart data for testing
|
||||
*/
|
||||
generateRandomData(depth: number = 3, maxChildren: number = 5): ChartDataItem[] {
|
||||
const generateNode = (currentDepth: number): ChartDataItem => {
|
||||
const name = `Node_${Math.random().toString(36).substr(2, 5)}`
|
||||
|
||||
@@ -30,7 +30,6 @@ class GlobalState {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Theory directly under main category
|
||||
this.theoryMappings.set(subcategory.name, {
|
||||
name: subcategory.name,
|
||||
category: mainCategory.name.toLowerCase()
|
||||
@@ -59,10 +58,8 @@ class GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const globalState = new GlobalState()
|
||||
|
||||
// Make it available on window for global access
|
||||
;(window as any).globalState = globalState
|
||||
|
||||
export default globalState
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { TheoryData } from '../types/theory'
|
||||
import { generateSlug } from './slugUtils'
|
||||
|
||||
// Simple cache for theory data
|
||||
const theoryCache = new Map<string, TheoryData>()
|
||||
|
||||
async function loadTheoryByName(theoryName: string): Promise<TheoryData> {
|
||||
// Check cache first
|
||||
if (theoryCache.has(theoryName)) {
|
||||
return theoryCache.get(theoryName)!
|
||||
}
|
||||
@@ -18,7 +16,6 @@ async function loadTheoryByName(theoryName: string): Promise<TheoryData> {
|
||||
throw new Error(`Failed to load theory data: ${response.statusText}`)
|
||||
}
|
||||
const theoryData = await response.json() as TheoryData
|
||||
// Cache the result
|
||||
theoryCache.set(theoryName, theoryData)
|
||||
return theoryData
|
||||
} catch (error) {
|
||||
@@ -66,13 +63,11 @@ export class Router {
|
||||
const category = segments[0]
|
||||
const theory = segments[1]
|
||||
|
||||
// Convert slug back to theory name for loading and callback
|
||||
const theoryName = theory
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('-')
|
||||
|
||||
// Show loading state with category and theory info
|
||||
this.onLoading?.(category, theory)
|
||||
|
||||
try {
|
||||
@@ -91,7 +86,6 @@ export class Router {
|
||||
}
|
||||
|
||||
private async loadTheory(category: string, theory: string): Promise<TheoryData> {
|
||||
// Convert slug back to theory name, preserving dashes
|
||||
const theoryName = theory
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
|
||||
Reference in New Issue
Block a user