🖲️ form submit

This commit is contained in:
Danilo Znamerovszkij
2025-10-04 18:21:40 +02:00
parent dcce9b15d3
commit b3e3c3e53f
11 changed files with 800 additions and 15 deletions

69
api/submit.ts Normal file
View File

@@ -0,0 +1,69 @@
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' })
}
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
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 telegramResponse = await fetch(`https://api.telegram.org/bot${tgToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_id: chatId,
text: telegramMessage,
parse_mode: 'Markdown'
}),
})
if (!telegramResponse.ok) {
console.error('Telegram API error:', await telegramResponse.text())
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',
message,
timestamp: new Date().toISOString()
})
}
// Return success response
return res.status(200).json({
success: true,
message: 'Form submitted successfully'
})
} catch (error) {
console.error('Form submission error:', error)
return res.status(500).json({
error: 'Internal server error',
message: 'Failed to process form submission'
})
}
}

View File

@@ -18,22 +18,30 @@
<!-- Navigation for testing -->
<div class="nav-buttons">
<div id="search-container"></div>
<button onclick="router.navigateToTheory('materialism', 'iit')">
Load IIT Theory
<button id="feedback-button" class="feedback-btn">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
Feedback
</button>
<button onclick="router.navigateToTheory('materialism', 'materialism')">
Load Materialism Theory
</button>
<button onclick="router.navigateToTheory('electromagnetic', 'zhang')">
Load Zhang Theory
</button>
<button onclick="router.goHome()">Home</button>
</div>
</div>
<!-- Item Details Panel -->
<div id="item-details"></div>
<!-- Form Popup -->
<div id="form-popup"></div>
<style>
* {
margin: 0;
@@ -105,6 +113,42 @@
min-width: 320px;
}
.feedback-btn {
display: flex;
align-items: center;
gap: 8px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
color: #3b82f6;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
white-space: nowrap;
&:hover {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
&:focus {
outline: 2px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
}
svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
}
#theory-search {
width: 100%;
padding: 12px 16px;

View File

@@ -0,0 +1,287 @@
.form-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
&.visible {
opacity: 1;
visibility: visible;
}
}
.form-popup {
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform: scale(0.9) translateY(20px);
transition: all 0.3s ease;
.form-overlay.visible & {
transform: scale(1) translateY(0);
}
}
.form-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 24px 0 24px;
border-bottom: 1px solid rgba(59, 130, 246, 0.1);
margin-bottom: 24px;
h3 {
color: #f1f5f9;
font-size: 20px;
font-weight: 600;
margin: 0;
}
}
.form-close {
background: none;
border: none;
color: #64748b;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
&:focus {
outline: 2px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
}
}
.form-content {
padding: 0 24px 24px 24px;
}
.form-group {
margin-bottom: 20px;
label {
display: block;
color: #f1f5f9;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
input,
textarea {
width: 100%;
padding: 12px 16px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
color: #f1f5f9;
font-size: 14px;
font-family: inherit;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
&::placeholder {
color: rgba(59, 130, 246, 0.6);
}
&:focus {
outline: none;
border-color: rgba(59, 130, 246, 0.4);
background: rgba(59, 130, 246, 0.15);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
&:invalid {
border-color: rgba(239, 68, 68, 0.3);
}
}
textarea {
resize: vertical;
min-height: 100px;
font-family: inherit;
}
}
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid rgba(59, 130, 246, 0.1);
}
.form-cancel,
.form-submit {
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid;
display: flex;
align-items: center;
gap: 8px;
min-width: 100px;
justify-content: center;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.form-cancel {
background: transparent;
border-color: rgba(107, 114, 128, 0.3);
color: #9ca3af;
&:hover:not(:disabled) {
background: rgba(107, 114, 128, 0.1);
border-color: rgba(107, 114, 128, 0.5);
color: #d1d5db;
}
&:focus {
outline: 2px solid rgba(107, 114, 128, 0.5);
outline-offset: 2px;
}
}
.form-submit {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.2);
color: #3b82f6;
&:hover:not(:disabled) {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
&:focus {
outline: 2px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
}
}
.submit-loading {
display: flex;
align-items: center;
gap: 8px;
svg {
animation: spin 1s linear infinite;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.form-error,
.form-success {
padding: 12px 16px;
border-radius: 8px;
margin-top: 16px;
font-size: 14px;
font-weight: 500;
text-align: center;
}
.form-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.form-success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
color: #86efac;
}
// Mobile responsiveness
@media (max-width: 768px) {
.form-popup {
width: 95%;
margin: 20px;
}
.form-header {
padding: 20px 20px 0 20px;
}
.form-content {
padding: 0 20px 20px 20px;
}
.form-actions {
flex-direction: column;
gap: 8px;
.form-cancel,
.form-submit {
width: 100%;
}
}
}
@media (max-width: 480px) {
.form-popup {
width: 100%;
height: 100%;
border-radius: 0;
max-height: none;
}
.form-header {
padding: 16px 16px 0 16px;
}
.form-content {
padding: 0 16px 16px 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-actions {
margin-top: 20px;
padding-top: 16px;
}
}

248
src/components/FormPopup.ts Normal file
View File

@@ -0,0 +1,248 @@
import { mockApiSubmit, isApiAvailable } from '@/utils/apiMock'
export class FormPopup {
private container: HTMLElement
private overlay: HTMLElement
private form: HTMLFormElement
private isVisible: boolean = false
constructor(containerId: string) {
const element = document.getElementById(containerId)
if (!element) {
throw new Error(`Container with id '${containerId}' not found`)
}
this.container = element
this.render()
this.attachEventListeners()
}
private render() {
this.container.innerHTML = `
<div class="form-overlay" id="form-overlay">
<div class="form-popup">
<div class="form-header">
<h3>Send Feedback</h3>
<button class="form-close" id="form-close" aria-label="Close form">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form class="form-content" id="feedback-form">
<div class="form-group">
<label for="feedback-name">Name (optional)</label>
<input
type="text"
id="feedback-name"
name="name"
placeholder="Your name"
aria-label="Your name"
/>
</div>
<div class="form-group">
<label for="feedback-email">Email (optional)</label>
<input
type="email"
id="feedback-email"
name="email"
placeholder="your@email.com"
aria-label="Your email"
/>
</div>
<div class="form-group">
<label for="feedback-message">Message *</label>
<textarea
id="feedback-message"
name="message"
placeholder="Share your thoughts, suggestions, or questions..."
rows="4"
required
aria-label="Your message"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="form-cancel" id="form-cancel">Cancel</button>
<button type="submit" class="form-submit" id="form-submit">
<span class="submit-text">Send Message</span>
<span class="submit-loading" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 11-6.219-8.56"/>
</svg>
Sending...
</span>
</button>
</div>
</form>
</div>
</div>
`
this.overlay = this.container.querySelector('#form-overlay') as HTMLElement
this.form = this.container.querySelector('#feedback-form') as HTMLFormElement
}
private attachEventListeners() {
// Close button
const closeBtn = this.container.querySelector('#form-close') as HTMLElement
closeBtn.addEventListener('click', () => this.hide())
// Cancel button
const cancelBtn = this.container.querySelector('#form-cancel') as HTMLElement
cancelBtn.addEventListener('click', () => this.hide())
// Overlay click to close
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.hide()
}
})
// Form submission
this.form.addEventListener('submit', (e) => this.handleSubmit(e))
// Escape key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isVisible) {
this.hide()
}
})
}
private async handleSubmit(e: Event) {
e.preventDefault()
const submitBtn = this.container.querySelector('#form-submit') as HTMLButtonElement
const submitText = this.container.querySelector('.submit-text') as HTMLElement
const submitLoading = this.container.querySelector('.submit-loading') as HTMLElement
// Get form data
const formData = new FormData(this.form)
const data = {
name: formData.get('name') as string || '',
email: formData.get('email') as string || '',
message: formData.get('message') as string
}
// Validate required message
if (!data.message.trim()) {
this.showError('Please enter a message')
return
}
// Show loading state
submitBtn.disabled = true
submitText.style.display = 'none'
submitLoading.style.display = 'flex'
try {
let response: any
if (isApiAvailable()) {
// Use real API when deployed
const apiResponse = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
if (apiResponse.ok) {
response = await apiResponse.json()
} else {
throw new Error('Failed to send message')
}
} else {
// Use mock API in development
response = await mockApiSubmit(data)
}
if (response.success) {
this.showSuccess('Message sent successfully!')
this.form.reset()
setTimeout(() => this.hide(), 1500)
} else {
throw new Error('Failed to send message')
}
} catch (error) {
console.error('Form submission error:', error)
this.showError('Failed to send message. Please try again.')
} finally {
// Reset button state
submitBtn.disabled = false
submitText.style.display = 'flex'
submitLoading.style.display = 'none'
}
}
private showError(message: string) {
// Remove existing error messages
const existingError = this.container.querySelector('.form-error')
if (existingError) {
existingError.remove()
}
// Create error message
const errorDiv = document.createElement('div')
errorDiv.className = 'form-error'
errorDiv.textContent = message
// Insert after form
this.form.parentNode?.insertBefore(errorDiv, this.form.nextSibling)
// Auto-remove after 5 seconds
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.remove()
}
}, 5000)
}
private showSuccess(message: string) {
// Remove existing messages
const existingMessage = this.container.querySelector('.form-success')
if (existingMessage) {
existingMessage.remove()
}
// Create success message
const successDiv = document.createElement('div')
successDiv.className = 'form-success'
successDiv.textContent = message
// Insert after form
this.form.parentNode?.insertBefore(successDiv, this.form.nextSibling)
}
show() {
this.isVisible = true
this.overlay.classList.add('visible')
document.body.style.overflow = 'hidden'
// Focus first input
const firstInput = this.form.querySelector('input, textarea') as HTMLElement
if (firstInput) {
setTimeout(() => firstInput.focus(), 100)
}
}
hide() {
this.isVisible = false
this.overlay.classList.remove('visible')
document.body.style.overflow = ''
// Clear form
this.form.reset()
// Remove any error/success messages
const errorMsg = this.container.querySelector('.form-error')
const successMsg = this.container.querySelector('.form-success')
if (errorMsg) errorMsg.remove()
if (successMsg) successMsg.remove()
}
isOpen(): boolean {
return this.isVisible
}
}

View File

@@ -468,7 +468,7 @@ export const getChartOptions = (): EChartsOption => {
return {
backgroundColor: 'transparent',
title: {
text: 'C-Atlas Chart',
// text: 'C-Atlas Chart',
left: 'center',
textStyle: {
color: '#fff'

View File

@@ -0,0 +1,33 @@
{
"theoryTitle": "Global Workspace Theory",
"category": "Cognitive Science",
"summary": "",
"overview": {
"purpose": "",
"focus": "",
"approach": ""
},
"components": {
"ontologicalStatus": "",
"explanatoryIdentity": "",
"functionAndEvolution": {
"function": "",
"evolution": ""
},
"causation": "",
"location": "",
"arguments": []
},
"bigQuestions": {
"ultimateMeaning": "",
"aiConsciousness": "",
"virtualImmortality": "",
"survivalBeyondDeath": ""
},
"philosophicalFocus": {
"mindBodyProblem": "",
"consciousnessNature": "",
"primitiveVsEmergent": "",
"reductionism": ""
}
}

View File

@@ -0,0 +1,33 @@
{
"theoryTitle": "Integrated World Model Theory",
"category": "Cognitive Science",
"summary": "",
"overview": {
"purpose": "",
"focus": "",
"approach": ""
},
"components": {
"ontologicalStatus": "",
"explanatoryIdentity": "",
"functionAndEvolution": {
"function": "",
"evolution": ""
},
"causation": "",
"location": "",
"arguments": []
},
"bigQuestions": {
"ultimateMeaning": "",
"aiConsciousness": "",
"virtualImmortality": "",
"survivalBeyondDeath": ""
},
"philosophicalFocus": {
"mindBodyProblem": "",
"consciousnessNature": "",
"primitiveVsEmergent": "",
"reductionism": ""
}
}

33
src/data/QuantumMind.json Normal file
View File

@@ -0,0 +1,33 @@
{
"theoryTitle": "Quantum Mind Theory",
"category": "Quantum Consciousness",
"summary": "",
"overview": {
"purpose": "",
"focus": "",
"approach": ""
},
"components": {
"ontologicalStatus": "",
"explanatoryIdentity": "",
"functionAndEvolution": {
"function": "",
"evolution": ""
},
"causation": "",
"location": "",
"arguments": []
},
"bigQuestions": {
"ultimateMeaning": "",
"aiConsciousness": "",
"virtualImmortality": "",
"survivalBeyondDeath": ""
},
"philosophicalFocus": {
"mindBodyProblem": "",
"consciousnessNature": "",
"primitiveVsEmergent": "",
"reductionism": ""
}
}

View File

@@ -5,10 +5,12 @@ import { SVGRenderer } from 'echarts/renderers'
import { CanvasRenderer } from 'echarts/renderers'
import { ChartExample } from '@/components/ChartExample'
import { SearchBar } from '@/components/SearchBar'
import { FormPopup } from '@/components/FormPopup'
import { Router } from '@/utils/routing'
// Import styles
import './components/ItemDetailsPanel.scss'
import './components/FormPopup.scss'
// Register the required components
echarts.use([TitleComponent, SunburstChart, SVGRenderer, CanvasRenderer])
@@ -27,6 +29,17 @@ const itemDetailsPanel = chartExample.getItemDetailsPanel()
// Initialize search bar
new SearchBar('search-container', router)
// Initialize form popup
const formPopup = new FormPopup('form-popup')
// Connect feedback button
const feedbackButton = document.getElementById('feedback-button')
if (feedbackButton) {
feedbackButton.addEventListener('click', () => {
formPopup.show()
})
}
// Set up close callback to navigate to home when panel is closed
itemDetailsPanel.setCloseCallback(() => {
router.goHome()

View File

@@ -10,7 +10,8 @@ body {
height: 100%;
overflow: hidden;
background: #1b1b1b;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
color: #f1f5f9;
}
@@ -83,22 +84,22 @@ button:focus-visible {
color: #1e293b;
background: #f8fafc;
}
body {
background: #f8fafc;
color: #1e293b;
}
#main {
background: transparent;
}
button {
background-color: rgba(59, 130, 246, 0.1);
color: #1d4ed8;
border-color: rgba(59, 130, 246, 0.3);
}
button:hover {
background-color: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.5);

24
src/utils/apiMock.ts Normal file
View File

@@ -0,0 +1,24 @@
// 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',
message: data.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'
}