summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-11-16 18:26:26 +0000
committersoryu <soryu@soryu.co>2025-11-16 18:27:53 +0000
commit679127a4f4685aa20fbf55fbd78c3a2e6832dabb (patch)
tree982644234c610821d38b556243738fb296497e62
parent889467a29b8194f2a51aec71534631cd00a246ce (diff)
downloadsoryu-679127a4f4685aa20fbf55fbd78c3a2e6832dabb.tar.gz
soryu-679127a4f4685aa20fbf55fbd78c3a2e6832dabb.zip
Landing page redesign: minimalist layout, Mission/Contact buttons, Login redirect; update title and favicon
Remove top level backend as it is used. Will be part of this monorepo, but structured differently
-rw-r--r--Cargo.toml16
-rw-r--r--frontend/index.html3
-rw-r--r--frontend/src/components/LandingPage.tsx237
-rw-r--r--frontend/src/styles/pc98.css20
-rw-r--r--src/main.rs172
5 files changed, 126 insertions, 322 deletions
diff --git a/Cargo.toml b/Cargo.toml
deleted file mode 100644
index fdbfbd1..0000000
--- a/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "soryu-backend"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-axum = { version = "0.7", features = ["macros", "json"] }
-tokio = { version = "1.37", features = ["rt-multi-thread", "macros"] }
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-tower-http = { version = "0.5", features = ["cors", "trace"] }
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
-tokio-stream = "0.1"
-futures = "0.3"
-chrono = { version = "0.4", default-features = false, features = ["clock"] }
diff --git a/frontend/index.html b/frontend/index.html
index 4bb8d23..095310b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
- <title>PC-98 VN Skeleton</title>
+ <title>soryu.co</title>
+ <link rel="icon" type="image/png" href="/logo/crane-logo-transparent.png" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DotGothic16&display=swap" rel="stylesheet">
</head>
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index f5dc55c..0489794 100644
--- a/frontend/src/components/LandingPage.tsx
+++ b/frontend/src/components/LandingPage.tsx
@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'
import { LoadingScreen } from './LoadingScreen'
import { HeartLogo } from './HeartLogo'
-// Using direct PNG logo on landing header
interface LandingPageProps {
onLogin: () => void
@@ -11,80 +10,95 @@ export function LandingPage({ onLogin }: LandingPageProps) {
const [loading, setLoading] = useState(false)
const [showLanding, setShowLanding] = useState(false)
const [isStandby, setIsStandby] = useState(true) // false = LIVE, true = STDBY
- const [asciiArt, setAsciiArt] = useState('')
- const [isGlitching, setIsGlitching] = useState(false)
- // Note: Removed VN preview and ASCII features to focus on an Art Deco/Nouveau landing.
+ const [velocity, setVelocity] = useState(0)
+ const [energy, setEnergy] = useState(0)
+ const [ramped, setRamped] = useState(false)
+ const [pendingAction, setPendingAction] = useState<null | 'makima' | 'mission'>(null)
- // Auto-fade in landing page content after component mounts
+ // Fade-in landing page content after mount
useEffect(() => {
- const timer = setTimeout(() => {
- setShowLanding(true)
- }, 500) // Delay before fading in content
-
+ const timer = setTimeout(() => setShowLanding(true), 500)
return () => clearTimeout(timer)
}, [])
- // Removed VN dialogue preview / typing effect.
-
- // Load ASCII art for overlay in hero (right/bottom)
- useEffect(() => {
- fetch('/ascii/ascii1.txt')
- .then((res) => res.text())
- .then((txt) => setAsciiArt(txt.replace(/\n+$/, '')))
- .catch(() => setAsciiArt(''))
- }, [])
-
- // Occasionally trigger a brief glitch on the word "futurist"
+ // Ramp up stats, then keep them fluctuating near max
useEffect(() => {
- let armTimer: number | undefined
- let activeTimer: number | undefined
- let cancelled = false
-
- const arm = () => {
- // Random delay between glitches: 0.8s–2s (slightly punchier)
- const delay = 800 + Math.random() * 1200
- armTimer = window.setTimeout(() => {
- setIsGlitching(true)
- // Glitch duration: 150ms–350ms (snappier)
- const dur = 150 + Math.random() * 200
- activeTimer = window.setTimeout(() => {
- setIsGlitching(false)
- if (!cancelled) arm()
- }, dur)
- }, delay)
- }
-
- arm()
+ const VELOCITY_MAX = 603
+ const ENERGY_MAX = 32
+
+ let rampInterval: number | undefined
+ let fluctuateInterval: number | undefined
+
+ // Ramp-up for ~2 seconds
+ const rampDurationMs = 2000
+ const tickMs = 30
+ const vStep = VELOCITY_MAX / (rampDurationMs / tickMs)
+ const eStep = ENERGY_MAX / (rampDurationMs / tickMs)
+
+ rampInterval = window.setInterval(() => {
+ setVelocity((v) => {
+ const next = v + vStep
+ return next >= VELOCITY_MAX ? VELOCITY_MAX : next
+ })
+ setEnergy((e) => {
+ const next = e + eStep
+ return next >= ENERGY_MAX ? ENERGY_MAX : next
+ })
+ }, tickMs)
+
+ const stopRamp = window.setTimeout(() => {
+ if (rampInterval) window.clearInterval(rampInterval)
+ setVelocity(VELOCITY_MAX)
+ setEnergy(ENERGY_MAX)
+ setRamped(true)
+
+ // Fluctuate near the top
+ fluctuateInterval = window.setInterval(() => {
+ setVelocity((v) => {
+ const min = VELOCITY_MAX - 18
+ const max = VELOCITY_MAX
+ const delta = (Math.random() - 0.5) * 6 // ±3
+ const next = Math.max(min, Math.min(max, v + delta))
+ return next
+ })
+ setEnergy((e) => {
+ const min = ENERGY_MAX - 2
+ const max = ENERGY_MAX
+ const delta = (Math.random() - 0.5) * 0.25 // ±0.125
+ const next = Math.max(min, Math.min(max, e + delta))
+ return next
+ })
+ }, 220)
+ }, rampDurationMs + 60)
return () => {
- cancelled = true
- if (armTimer) clearTimeout(armTimer)
- if (activeTimer) clearTimeout(activeTimer)
+ if (rampInterval) window.clearInterval(rampInterval)
+ if (fluctuateInterval) window.clearInterval(fluctuateInterval)
}
}, [])
-
- // Handle loading screen completion
const handleLoadingComplete = () => {
- // Call the original onLogin immediately to transition to VN page
- // Don't reset loading states as we're leaving the landing page
+ if (pendingAction === 'makima') {
+ window.location.assign('https://makima.jp')
+ return
+ }
onLogin()
}
- // Handle login button click
- const handleLogin = () => {
+ const handleMakima = () => {
+ setPendingAction('makima')
setLoading(true)
}
- // Removed ASCII art effects and rendering.
+ const handleMission = () => {
+ // Placeholder action for now
+ setPendingAction('mission')
+ }
return (
<div>
- {loading && (
- <LoadingScreen onComplete={handleLoadingComplete} />
- )}
-
- {/* Floating Header Bar - Hidden during loading */}
+ {loading && <LoadingScreen onComplete={handleLoadingComplete} />}
+
{!loading && (
<div className={`floating-header ${showLanding ? 'fade-in' : 'hidden'}`}>
<div className="header-content">
@@ -94,7 +108,11 @@ export function LandingPage({ onLogin }: LandingPageProps) {
alt="Soryu"
height={40}
className="brand-mark"
- onError={(e) => { const img = (e.currentTarget as HTMLImageElement); img.onerror = null; img.src = '/logo/crane-logo.png'; }}
+ onError={(e) => {
+ const img = e.currentTarget as HTMLImageElement
+ img.onerror = null
+ img.src = '/logo/crane-logo.png'
+ }}
/>
</div>
<div className="header-center">
@@ -121,20 +139,17 @@ export function LandingPage({ onLogin }: LandingPageProps) {
</div>
</div>
)}
-
+
<div className={`modern-landing-page manga-style ${showLanding && !loading ? 'fade-in' : 'hidden'}`}>
- {/* Retro-futuristic page background */}
- <div className="rf-page-bg" aria-hidden="true">
- <div className="rf-page-speedlines layer-a"></div>
- <div className="rf-page-speedlines layer-b"></div>
+ {/* Background GIF fills the main body */}
+ <div className="background-only" aria-hidden="true">
+ <img src="/background-animation.gif" alt="" className="background-gif" />
</div>
- {/* Taisho Magazine Cover Backdrop */}
- <div className="taisho-cover">
- <div className="cover-backdrop" aria-hidden="true"></div>
- {/* Cover Content Grid */}
+ {/* Minimal overlay: masthead, issue badge, and CTA */}
+ <div className="taisho-cover">
<div className="cover-content">
- {/* Vertical Masthead (magazine-style) */}
+ {/* Masthead + Issue badge (kept) */}
<div className="masthead">
<div className="masthead-vertical">
<span className="jp">そりゅう</span>
@@ -143,73 +158,39 @@ export function LandingPage({ onLogin }: LandingPageProps) {
<div className="issue-badge">かはいい Vol.01</div>
</div>
- {/* Central Hero - full-frame retro-futuristic */}
- <div className="hero">
- <div className="hero-frame taisho-modern-frame">
- <div className="hero-inner hero-fill">
- {/* Retro-futuristic racing hero content */}
- <div className="rf-hero" aria-hidden="true">
- <div className="rf-speedlines layer-a"></div>
- <div className="rf-speedlines layer-b"></div>
- <div className="rf-hero-content">
- <div className="rf-badge">Engineering • Systems • para bellum</div>
- <h2 className="rf-headline">
- Mission driven{' '}
- <span
- className={`glitch-word ${isGlitching ? 'is-glitching' : ''}`}
- data-text="futurist"
- >
- futurist
- </span>{' '}
- technology
- </h2>
- <p className="rf-tagline">Avant-garde. Aesthetic Engineering. A Race of Steel</p>
- <div className="rf-stats">
- <div className="rf-stat"><span className="label">Velocity</span><span className="value">0–603 km/h</span></div>
- <div className="rf-stat"><span className="label">Energy</span><span className="value">32 MJ</span></div>
- <div className="rf-stat"><span className="label">Flow</span><span className="value">MAX</span></div>
- </div>
- </div>
- <div className="rf-accent-diagonal"></div>
- {/* ASCII overlay (transparent, gradient text) */}
- {asciiArt && (
- <div className="rf-ascii-overlay" aria-hidden="true">
- {asciiArt.split('\n').map((line, i) => (
- <div key={i} className="ascii-line">
- {Array.from(line).map((ch, j) => (
- <span
- key={`${i}-${j}`}
- className="ascii-char"
- style={{
- '--delay': `${(i * 0.08 + j * 0.01)}s`,
- } as React.CSSProperties}
- >
- {ch}
- </span>
- ))}
- </div>
- ))}
- </div>
- )}
- </div>
- </div>
+ {/* Empty hero area to preserve grid; background sits behind */}
+ <div className="hero" />
+
+ {/* CTA row spanning full width: left Mission/Contact, right Login */}
+ <div className="cta-area">
+ <div className="cta-left">
+ <button className="taisho-cta" onClick={handleMission}>
+ <span className="cta-text">Mission</span>
+ </button>
+ <button className="taisho-cta" onClick={() => {/* placeholder contact */}}>
+ <span className="cta-text">Contact</span>
+ </button>
+ </div>
+ <div className="cta-right">
+ <button className="taisho-cta" onClick={handleMakima}>
+ <span className="cta-icon">▶</span>
+ <span className="cta-text">Login</span>
+ </button>
</div>
</div>
+ </div>
+ </div>
- {/* CTA */}
- <div className="cta-area">
- <button className="taisho-cta" onClick={handleLogin}>
- <span className="cta-icon">▶</span>
- <span className="cta-text">Enter</span>
- </button>
+ {/* Bottom stats: Velocity + Energy only */}
+ <div className="bottom-stats">
+ <div className="rf-stats">
+ <div className="rf-stat">
+ <span className="label">Velocity</span>
+ <span className="value">{Math.round(velocity)} km/h</span>
</div>
- {/* Visitor Counter */}
- <div className="visit-counter" aria-label="visitor counter">
- <img
- src="https://count.getloli.com/get/@soryu-landing?theme=booru-jaypee&darkmode=0"
- alt="visit counter"
- referrerPolicy="no-referrer-when-downgrade"
- />
+ <div className="rf-stat">
+ <span className="label">Energy</span>
+ <span className="value">{energy.toFixed(1)} MJ</span>
</div>
</div>
</div>
diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css
index 63e1996..39b8392 100644
--- a/frontend/src/styles/pc98.css
+++ b/frontend/src/styles/pc98.css
@@ -641,6 +641,14 @@ button:focus-visible {
pointer-events: none;
}
+/* Minimal landing background */
+.background-only { position: absolute; inset: 0; z-index: 0; }
+.background-only .background-gif { width: 100%; height: 100%; object-fit: cover; display: block; }
+
+/* Bottom overlay for Velocity/Energy */
+.bottom-stats { position: absolute; left: 0; right: 0; bottom: 16px; display: flex; justify-content: center; z-index: 2; pointer-events: none; }
+.bottom-stats .rf-stats { background: rgba(10, 12, 20, 0.55); border: 1px solid rgba(102,204,255,0.6); padding: 8px 12px; border-radius: 6px; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); margin: 0; pointer-events: auto; }
+
/* Floating Header Bar */
.floating-header {
position: fixed;
@@ -1670,11 +1678,11 @@ button:focus-visible {
z-index: 1;
height: 100%;
display: grid;
- grid-template-columns: 1fr 220px; /* hero left, masthead right */
- grid-template-rows: 1fr auto; /* CTA sits below hero */
+ grid-template-columns: 220px 1fr; /* masthead left, hero right */
+ grid-template-rows: 1fr auto; /* CTA spans bottom */
grid-template-areas:
- "hero masthead"
- "hero cta";
+ "masthead hero"
+ "cta cta";
gap: 28px;
padding: 40px 48px;
}
@@ -2128,7 +2136,9 @@ button:focus-visible {
/* .ascii-section / .ascii-frame / .ascii-art-container not used */
/* CTA */
-.cta-area { grid-area: cta; align-self: end; display: flex; justify-content: flex-end; }
+.cta-area { grid-area: cta; align-self: end; display: flex; justify-content: space-between; align-items: center; gap: 16px; }
+.cta-left { display: flex; flex-direction: column; gap: 12px; }
+.cta-right { display: flex; align-items: center; }
.taisho-cta {
position: relative;
background: var(--ink);
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index a2bf354..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-use std::{convert::Infallible, env, net::SocketAddr, time::Duration};
-
-use axum::{
- extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State},
- http::Method,
- response::IntoResponse,
- routing::{get, post},
- Json, Router,
-};
-use axum::response::sse::{Event, KeepAlive, Sse};
-use serde::{Deserialize, Serialize};
-use tower_http::{cors::{Any, CorsLayer}, trace::TraceLayer};
-use tracing::info;
-use tracing_subscriber::EnvFilter;
-use tokio_stream::{wrappers::IntervalStream, StreamExt as _};
-use futures::StreamExt as _; // for websocket split/next
-use futures::stream::Stream; // for SSE return type
-
-#[derive(Clone, Default)]
-struct AppState {}
-
-#[derive(Serialize)]
-struct HealthResponse {
- status: &'static str,
-}
-
-#[derive(Serialize)]
-struct HelloResponse {
- message: &'static str,
-}
-
-#[derive(Deserialize, Serialize)]
-struct EchoPayload {
- #[serde(flatten)]
- rest: serde_json::Value,
-}
-
-#[tokio::main]
-async fn main() {
- // Logging setup
- let filter = EnvFilter::try_from_default_env()
- .unwrap_or_else(|_| EnvFilter::new("info,tower_http=info,axum::rejection=trace"));
- tracing_subscriber::fmt()
- .with_env_filter(filter)
- .with_target(false)
- .compact()
- .init();
-
- // Shared app state (extend as needed)
- let state = AppState::default();
-
- // CORS to allow local frontend dev at 5173 and others
- let cors = CorsLayer::new()
- .allow_origin(Any)
- .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
- .allow_headers(Any);
-
- // Router
- let app = Router::new()
- .route("/", get(root))
- .route("/health", get(health))
- .route("/api/hello", get(hello))
- .route("/api/echo", post(echo))
- .route("/api/stream/transcript", get(stream_transcript_sse))
- .route("/ws", get(ws_handler))
- .with_state(state)
- .layer(cors)
- .layer(TraceLayer::new_for_http());
-
- // Bind address
- let port = env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080);
- let addr = SocketAddr::from(([0, 0, 0, 0], port));
- info!(%addr, "starting soryu backend");
- let listener = tokio::net::TcpListener::bind(addr).await.expect("bind port");
- axum::serve(listener, app).await.expect("server error");
-}
-
-async fn root() -> impl IntoResponse {
- Json(serde_json::json!({
- "service": "soryu-backend",
- "endpoints": [
- "/health",
- "/api/hello",
- "/api/echo",
- "/api/stream/transcript",
- "/ws"
- ],
- }))
-}
-
-async fn health() -> impl IntoResponse {
- Json(HealthResponse { status: "ok" })
-}
-
-async fn hello(State(_state): State<AppState>) -> impl IntoResponse {
- Json(HelloResponse { message: "Hello from Soryu backend" })
-}
-
-async fn echo(Json(body): Json<serde_json::Value>) -> impl IntoResponse {
- Json(EchoPayload { rest: body })
-}
-
-// ---
-// Streaming transcript (SSE) skeleton
-// Provides a low-latency server-sent events stream that emits example
-// transcript chunks every ~250ms.
-async fn stream_transcript_sse(State(_state): State<AppState>) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
- const SAMPLE_LINES: &[&str] = &[
- "speaker_a: hey there, can you hear me?",
- "speaker_b: loud and clear — let's begin.",
- "speaker_a: streaming transcript looks smooth so far.",
- "speaker_b: agreed, latency feels low.",
- "speaker_a: wrapping up the demo now.",
- ];
-
- let mut idx = 0usize;
- let interval = tokio::time::interval(Duration::from_millis(250));
- let stream = IntervalStream::new(interval).map(move |_| {
- let line = SAMPLE_LINES[idx % SAMPLE_LINES.len()];
- idx += 1;
- let data = serde_json::json!({
- "type": "transcript_chunk",
- "text": line,
- "ts_ms": chrono::Utc::now().timestamp_millis(),
- });
- Ok(Event::default().json_data(&data).unwrap())
- });
-
- Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10)))
-}
-
-// ---
-// WebSocket transcript stream skeleton
-// Sends example transcript messages on connect; ignores inbound messages.
-async fn ws_handler(ws: WebSocketUpgrade, State(_state): State<AppState>) -> impl IntoResponse {
- ws.on_upgrade(handle_socket)
-}
-
-async fn handle_socket(mut socket: WebSocket) {
- const SAMPLE_LINES: &[&str] = &[
- "speaker_a: hey there, can you hear me?",
- "speaker_b: loud and clear — let's begin.",
- "speaker_a: streaming transcript looks smooth so far.",
- "speaker_b: agreed, latency feels low.",
- "speaker_a: wrapping up the demo now.",
- ];
-
- // Spawn a task to drain inbound messages (optional for skeleton)
- let mut recv_socket = socket.split().1;
- tokio::spawn(async move {
- while let Some(Ok(_msg)) = recv_socket.next().await {
- // Ignore inbound for skeleton; handle pings/acks here if needed
- }
- });
-
- // Send a small transcript stream
- for line in SAMPLE_LINES {
- let payload = serde_json::json!({
- "type": "transcript_chunk",
- "text": line,
- "ts_ms": chrono::Utc::now().timestamp_millis(),
- })
- .to_string();
- if socket.send(Message::Text(payload)).await.is_err() {
- return;
- }
- tokio::time::sleep(Duration::from_millis(250)).await;
- }
-
- let done = serde_json::json!({"type":"done"}).to_string();
- let _ = socket.send(Message::Text(done)).await;
-}