🧑🏼‍💻 integrate analytics, add About button, and enhance UI elements

This commit is contained in:
Danilo Znamerovszkij
2025-10-23 23:04:39 +02:00
parent 490e392ea4
commit c981a07a2f
22 changed files with 659 additions and 211 deletions

View File

197
README.md
View File

@@ -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

View File

@@ -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'

View File

@@ -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
View File

@@ -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",

View File

@@ -19,6 +19,7 @@
},
"dependencies": {
"echarts": "^6.0.0",
"mixpanel-browser": "^2.71.0",
"vite": "^5.4.0",
"vite-tsconfig-paths": "^5.1.4"
},

View File

@@ -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)
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 = `

View File

@@ -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 === ' ') {

View File

@@ -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)
})
}

View File

@@ -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()

View File

@@ -1,4 +1,3 @@
// Static mapping for all theories
const staticTheoryNames: Record<string, string> = {
// 1.1 Philosophical Theories
'Eliminative': 'Eliminative Materialism / Illusionism',

View File

@@ -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)
})

View File

@@ -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
View 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

View File

@@ -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'
}

View File

@@ -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)}`

View File

@@ -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

View File

@@ -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))