diff options
| author | soryu <soryu@soryu.co> | 2025-11-15 18:00:09 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2025-11-15 18:00:09 +0000 |
| commit | 3e7b2beca1136a42700a7e1aebfe4c0fb2861a00 (patch) | |
| tree | 6c896c31820681e360e50a73839fc2284c043dea /frontend/src/components/VNInterface.tsx | |
| download | soryu-3e7b2beca1136a42700a7e1aebfe4c0fb2861a00.tar.gz soryu-3e7b2beca1136a42700a7e1aebfe4c0fb2861a00.zip | |
Initial commit
Diffstat (limited to 'frontend/src/components/VNInterface.tsx')
| -rw-r--r-- | frontend/src/components/VNInterface.tsx | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx new file mode 100644 index 0000000..be71d27 --- /dev/null +++ b/frontend/src/components/VNInterface.tsx @@ -0,0 +1,208 @@ +import React, { useEffect } from 'react' +import { useStore } from '@nanostores/react' +import { + isStandbyStore, + currentTimeStore, + weatherStore, + showChoicesStore, + showSettingsModalStore, + isVisibleStore, + yenBalanceStore, + toggleStandby, + toggleShowChoices, + updateTime +} from '../stores' + +interface VNInterfaceProps { + onLogout: () => void +} + +export function VNInterface({ onLogout }: VNInterfaceProps) { + const isStandby = useStore(isStandbyStore) + const currentTime = useStore(currentTimeStore) + const weather = useStore(weatherStore) + const showChoices = useStore(showChoicesStore) + const showSettingsModal = useStore(showSettingsModalStore) + const isVisible = useStore(isVisibleStore) + const yenBalance = useStore(yenBalanceStore) + + // Fade in effect on mount + useEffect(() => { + const timer = setTimeout(() => { + isVisibleStore.set(true) + }, 100) + return () => clearTimeout(timer) + }, []) + + // Update clock every second (Japan Time) + useEffect(() => { + const timer = setInterval(() => { + const now = new Date() + // Convert to Japan Time (UTC+9) + const japanTime = new Date(now.getTime() + (now.getTimezoneOffset() * 60000) + (9 * 3600000)) + updateTime() + }, 1000) + return () => clearInterval(timer) + }, []) + + return ( + <div className={`vn-interface ${isVisible ? 'fade-in' : 'fade-out'}`}> + {/* Background */} + <div className="vn-background"> + <img + src="/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png" + alt="Background image" + className="background-image" + /> + </div> + + {/* Combined Info Panel (Top Right) */} + <div className="floating-info-panel"> + <div className="info-panel-content"> + {/* Weather Section */} + <div className="weather-section"> + <div className="weather-icon">🌤️</div> + <div className="weather-details"> + <div className="weather-location">Tokyo</div> + <div className="weather-temp">22°C Sunny</div> + </div> + </div> + + {/* Time Section */} + <div className="time-section"> + <div className="japan-date">{currentTime.toLocaleDateString('ja-JP', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'short' + })}</div> + <div className="japan-time">{currentTime.toLocaleTimeString('ja-JP', { + hour12: false, + hour: '2-digit', + minute: '2-digit' + })}</div> + </div> + + {/* Status Section */} + <div className="status-section"> + <div className="status-item"> + <span className="info-label">Balance:</span> + <span className="info-value yen-balance">¥{yenBalance.toLocaleString()}</span> + </div> + <div className="status-item"> + <span className="info-label">System:</span> + <span + className="info-value live-status clickable" + onClick={toggleStandby} + title="Click to toggle between LIVE and STANDBY" + > + <span className={`status-dot ${isStandby ? 'standby' : 'live'}`}></span> + {isStandby ? 'STDBY' : 'LIVE'} + </span> + </div> + </div> + </div> + </div> + + {/* Main VN Viewport */} + <div className="vn-viewport"> + <div className="vn-content"> + </div> + </div> + + {/* Dialogue Panel (Bottom) */} + <div className="floating-dialogue-panel"> + <div className="dialogue-content"> + <div className="dialogue-speaker">???</div> + <div className="dialogue-text"> + A warm CRT glow fills the room. A figure turns towards you... + </div> + </div> + </div> + + {/* Input/Choice Panel (Bottom) */} + <div className="floating-input-panel"> + <div className="input-content"> + {!showChoices ? ( + // Text Input Mode + <input + type="text" + className="vn-text-input" + placeholder="Type your response..." + onKeyPress={(e) => { + if (e.key === 'Enter') { + const target = e.target as HTMLInputElement; + if (target.value.trim()) { + console.log('User input:', target.value); + target.value = ''; + } + } + }} + /> + ) : ( + // Choice Options Mode + <div className="choice-buttons"> + <button className="choice-btn" onClick={() => console.log('Choice: Hello?')}>"Hello?"</button> + <button className="choice-btn" onClick={() => console.log('Choice: Who are you?')}>"Who are you?"</button> + <button className="choice-btn" onClick={() => console.log('Choice: Stay silent')}>(Stay silent)</button> + </div> + )} + + {/* Toggle Button */} + <button + className="toggle-input-btn" + onClick={toggleShowChoices} + title={showChoices ? "Switch to text input" : "Switch to choice options"} + > + {showChoices ? "⎀" : "≡"} + </button> + </div> + </div> + + {/* Floating Settings Button */} + <button className="floating-logout-btn" onClick={() => showSettingsModalStore.set(true)}> + <span className="btn-icon">⚙</span> + <span className="btn-text">Settings</span> + </button> + + {/* Settings Modal */} + {showSettingsModal && ( + <div className="modal-overlay" onClick={() => showSettingsModalStore.set(false)}> + <div className="settings-modal" onClick={(e) => e.stopPropagation()}> + <div className="modal-header"> + <h2 className="modal-title">Settings</h2> + <button className="modal-close-btn" onClick={() => showSettingsModalStore.set(false)}> + × + </button> + </div> + <div className="modal-content"> + <div className="settings-section"> + <h3>Display Options</h3> + <div className="setting-item"> + <label> + <input type="checkbox" defaultChecked /> Enable animations + </label> + </div> + </div> + <div className="settings-section"> + <h3>Audio</h3> + <div className="setting-item"> + <label>Master Volume</label> + <input type="range" min="0" max="100" defaultValue="75" /> + </div> + </div> + </div> + <div className="modal-footer"> + <button className="modal-btn secondary" onClick={() => showSettingsModalStore.set(false)}> + Cancel + </button> + <button className="modal-btn logout" onClick={() => { showSettingsModalStore.set(false); onLogout(); }}> + Logout + </button> + </div> + </div> + </div> + )} + </div> + ) +} |
