From 15a18e45b418dbb664999c993247c16dc8031ef9 Mon Sep 17 00:00:00 2001 From: rail Date: Sat, 25 Oct 2025 23:14:46 +0300 Subject: [PATCH] Rewrite landing page in Rust + WebAssembly - Complete rewrite from HTML/CSS/JS to Rust + WASM - Uses vanilla wasm-bindgen with web-sys (no framework) - Minimal bundle size: ~19KB WASM + 9KB JS - Modern, clean redesign with dark theme - Local builds, dist files committed for fast CI Technical changes: - Added Cargo.toml with wasm-bindgen, web-sys dependencies - Implemented landing page in src/lib.rs using web_sys DOM API - Created minimal index.html shell for WASM loading - Designed modern CSS with responsive layout - Archived old HTML files to html-old/ - Updated Dockerfile to copy dist/ instead of html/ - Updated build process in README.md - Documented migration in CHANGELOG.md Build process: 1. cargo build --release --target wasm32-unknown-unknown 2. wasm-bindgen --out-dir dist --target web 3. Copy static assets to dist/ 4. Commit dist/ to git for fast CI deployment Benefits: - Near-native performance - Type safety from Rust - Smaller bundle size - Modern web technology stack --- .cargo/config.toml | 5 + .gitignore | 18 ++ Cargo.toml | 28 +++ Dockerfile | 2 +- README.md | 81 ++++++-- dist/index.html | 35 ++++ dist/railwayka_landing.d.ts | 33 +++ dist/railwayka_landing.js | 275 +++++++++++++++++++++++++ dist/railwayka_landing_bg.wasm | Bin 0 -> 19425 bytes dist/railwayka_landing_bg.wasm.d.ts | 6 + dist/style.css | 299 ++++++++++++++++++++++++++++ {html => html-old}/index.html | 0 {html => html-old}/script.js | 0 {html => html-old}/style.css | 0 index.html | 35 ++++ src/lib.rs | 240 ++++++++++++++++++++++ style.css | 299 ++++++++++++++++++++++++++++ 17 files changed, 1338 insertions(+), 18 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 Cargo.toml create mode 100644 dist/index.html create mode 100644 dist/railwayka_landing.d.ts create mode 100644 dist/railwayka_landing.js create mode 100644 dist/railwayka_landing_bg.wasm create mode 100644 dist/railwayka_landing_bg.wasm.d.ts create mode 100644 dist/style.css rename {html => html-old}/index.html (100%) rename {html => html-old}/script.js (100%) rename {html => html-old}/style.css (100%) create mode 100644 index.html create mode 100644 src/lib.rs create mode 100644 style.css diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..f35edc5 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[target.wasm32-unknown-unknown] +rustflags = ["-C", "link-arg=-s"] diff --git a/.gitignore b/.gitignore index 87bf182..f21c759 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,20 @@ +# Environment files .env .env.bak + +# Rust build artifacts +target/ +Cargo.lock + +# Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Note: We commit dist/ since we build locally for faster CI diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..66cdbb1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "railwayka-landing" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.104" +wasm-bindgen-futures = "0.4.54" + +[dependencies.web-sys] +version = "0.3.81" +features = [ + "console", + "Document", + "Element", + "HtmlElement", + "Node", + "Window", + "Location", +] + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 diff --git a/Dockerfile b/Dockerfile index 7696acc..14be65c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ ARG IMAGE_VER=1.27-alpine FROM --platform=$BUILDPLATFORM ${IMAGE}:${IMAGE_VER} -COPY html /usr/share/nginx/html +COPY dist /usr/share/nginx/html diff --git a/README.md b/README.md index c38465b..36cd935 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ # railwayka.ru Landing Page -Business card website for railwayka.ru infrastructure, deployed via [dcape](https://github.com/dopos/dcape) GitOps platform. +Modern business card website for railwayka.ru infrastructure, built with **Rust + WebAssembly** and deployed via [dcape](https://github.com/dopos/dcape) GitOps platform. ## Features -- Modern dark theme with animated stars background +- 🦀 **Rust + WebAssembly** - High-performance, compiled to WASM +- Modern dark theme with clean design - Service cards showcasing current and planned services - Technology stack descriptions -- GitOps workflow visualization -- Konami code easter egg -- Responsive design +- Fully responsive layout +- Minimal bundle size (~19KB WASM) ## Technology Stack +- **Rust**: Core application logic +- **WebAssembly**: Fast, portable binary format +- **wasm-bindgen**: Rust-WASM bindings +- **web-sys**: Web API bindings for Rust - **Nginx**: Alpine-based web server - **Docker**: Containerized deployment - **Traefik**: Automatic HTTPS via Let's Encrypt @@ -44,32 +48,75 @@ make docker-build make up ``` -## Local Development +## Building from Source -To run locally: +### Prerequisites + +- Rust (latest stable) - install from [rustup.rs](https://rustup.rs/) +- wasm-bindgen-cli - `cargo install wasm-bindgen-cli` +- wasm32 target - `rustup target add wasm32-unknown-unknown` + +### Build Steps ```bash -# Serve files with any web server -cd html +# 1. Build Rust to WASM +cargo build --release --target wasm32-unknown-unknown + +# 2. Generate JS bindings +wasm-bindgen --out-dir dist --target web \ + target/wasm32-unknown-unknown/release/railwayka_landing.wasm + +# 3. Copy static assets +cp index.html style.css dist/ + +# 4. Serve locally +cd dist python3 -m http.server 8000 # Visit http://localhost:8000 ``` -## Structure +### Quick Build Script + +For convenience, you can use this one-liner: + +```bash +cargo build --release --target wasm32-unknown-unknown && \ +wasm-bindgen --out-dir dist --target web target/wasm32-unknown-unknown/release/railwayka_landing.wasm && \ +cp index.html style.css dist/ +``` + +## Project Structure ``` . -├── Makefile # Dcape app Makefile -├── docker-compose.yml # Service definition -├── Dockerfile # Custom nginx image -├── .woodpecker.yml # CI/CD pipeline -├── html/ # Static website files +├── Cargo.toml # Rust project manifest +├── .cargo/ +│ └── config.toml # Rust build configuration +├── src/ +│ └── lib.rs # Main Rust/WASM code +├── index.html # HTML shell +├── style.css # Styling +├── dist/ # Built artifacts (committed to git) │ ├── index.html │ ├── style.css -│ └── script.js -└── README.md +│ ├── railwayka_landing.js +│ ├── railwayka_landing_bg.wasm +│ └── *.d.ts +├── html-old/ # Archived old HTML version +├── Makefile # Dcape app Makefile +├── docker-compose.yml # Service definition +├── Dockerfile # Nginx deployment +└── .woodpecker.yml # CI/CD pipeline ``` +## Why Rust + WASM? + +- **Performance**: Near-native execution speed +- **Size**: Optimized bundle (~19KB WASM + 9KB JS) +- **Type Safety**: Rust's type system prevents common bugs +- **Modern**: Cutting-edge web technology +- **Learning**: Great opportunity to explore WASM ecosystem + ## License MIT diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..a152394 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,35 @@ + + + + + + + railwayka.ru + + + +
+
Loading...
+
+ + + + diff --git a/dist/railwayka_landing.d.ts b/dist/railwayka_landing.d.ts new file mode 100644 index 0000000..66b17eb --- /dev/null +++ b/dist/railwayka_landing.d.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +export function main(): void; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly main: () => void; + readonly __wbindgen_export_0: (a: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. +* +* @returns {InitOutput} +*/ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. +* +* @returns {Promise} +*/ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/dist/railwayka_landing.js b/dist/railwayka_landing.js new file mode 100644 index 0000000..9c1168b --- /dev/null +++ b/dist/railwayka_landing.js @@ -0,0 +1,275 @@ +let wasm; + +let heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_export_0(addHeapObject(e)); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +export function main() { + wasm.main(); +} + +const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_appendChild_87a6cc0aeb132c06 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).appendChild(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_body_8822ca55cb3730d2 = function(arg0) { + const ret = getObject(arg0).body; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_call_13410aac570ffff7 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) { + const ret = getObject(arg0).document; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_Window_12d20d558ef92592 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) { + console.log(getObject(arg0)); + }; + imports.wbg.__wbg_newnoargs_254190557c45b4ec = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) { + getObject(arg0).className = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) { + getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) { + getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_THIS_f0a4409105898184 = function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_static_accessor_SELF_995b214ae681ff99 = function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_static_accessor_WINDOW_cde3890479c675ea = function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbg_wbindgenrethrow_01815c9239d70cc2 = function(arg0) { + throw takeObject(arg0); + }; + imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('railwayka_landing_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/dist/railwayka_landing_bg.wasm b/dist/railwayka_landing_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..37de7d2d725d2cf182fa17334c62a0b67fa5d503 GIT binary patch literal 19425 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¨l`D(~%uU7Phyy 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 O|G`4H6ZFJR@P7fv+{1PN literal 0 HcmV?d00001 diff --git a/dist/railwayka_landing_bg.wasm.d.ts b/dist/railwayka_landing_bg.wasm.d.ts new file mode 100644 index 0000000..13cde68 --- /dev/null +++ b/dist/railwayka_landing_bg.wasm.d.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const main: () => void; +export const __wbindgen_export_0: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 0000000..0f8af89 --- /dev/null +++ b/dist/style.css @@ -0,0 +1,299 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-dark: #0a0e17; + --bg-card: #1a1f2e; + --bg-card-hover: #252a3a; + --text-primary: #e0e6ed; + --text-secondary: #9ca3af; + --accent: #3b82f6; + --accent-hover: #2563eb; + --rust: #f74c00; + --border: #2d3748; + --success: #10b981; + --planned: #6366f1; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif; + background: var(--bg-dark); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +/* Loading screen */ +#loading { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--bg-dark); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loader { + font-size: 1.2rem; + color: var(--text-secondary); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +.error { + padding: 2rem; + text-align: center; + color: var(--rust); +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; +} + +/* Hero Section */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%); + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%); +} + +.hero .container { + position: relative; + z-index: 1; +} + +.hero-title { + font-size: clamp(3rem, 8vw, 6rem); + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(135deg, var(--accent) 0%, var(--planned) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: clamp(1.2rem, 3vw, 1.5rem); + color: var(--text-secondary); + margin-bottom: 2rem; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.hero-badge { + display: inline-block; + padding: 0.75rem 1.5rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 2rem; + font-size: 1rem; + color: var(--text-primary); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +/* Section Styles */ +section { + padding: 5rem 0; +} + +.section-title { + font-size: 2.5rem; + margin-bottom: 3rem; + text-align: center; + color: var(--text-primary); +} + +/* Services Section */ +.services { + background: var(--bg-dark); +} + +.services-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.service-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.3s ease; + text-decoration: none; + color: inherit; + display: block; +} + +.service-card:hover { + background: var(--bg-card-hover); + border-color: var(--accent); + transform: translateY(-4px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2); +} + +.service-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.service-card h3 { + font-size: 1.25rem; + color: var(--text-primary); + flex: 1; +} + +.status-badge { + font-size: 1.5rem; + flex-shrink: 0; + margin-left: 0.5rem; +} + +.service-description { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.6; +} + +/* Tech Section */ +.tech { + background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%); +} + +.tech-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.tech-item { + text-align: center; + padding: 2rem 1rem; +} + +.tech-item h3 { + font-size: 1.5rem; + margin-bottom: 0.75rem; + color: var(--accent); +} + +.tech-item p { + color: var(--text-secondary); + font-size: 0.95rem; +} + +/* Footer */ +.footer { + background: var(--bg-card); + border-top: 1px solid var(--border); + padding: 3rem 0; + text-align: center; +} + +.footer p { + color: var(--text-secondary); + margin-bottom: 1.5rem; +} + +.rust-love { + color: var(--rust); + font-weight: 600; +} + +.footer-links { + display: flex; + justify-content: center; + gap: 2rem; + flex-wrap: wrap; +} + +.footer-links a { + color: var(--accent); + text-decoration: none; + transition: color 0.3s ease; + font-size: 0.95rem; +} + +.footer-links a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + padding: 0 1.5rem; + } + + section { + padding: 3rem 0; + } + + .services-grid, + .tech-grid { + grid-template-columns: 1fr; + } + + .hero { + min-height: 80vh; + } + + .footer-links { + flex-direction: column; + gap: 1rem; + } +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Selection color */ +::selection { + background: var(--accent); + color: white; +} + +::-moz-selection { + background: var(--accent); + color: white; +} diff --git a/html/index.html b/html-old/index.html similarity index 100% rename from html/index.html rename to html-old/index.html diff --git a/html/script.js b/html-old/script.js similarity index 100% rename from html/script.js rename to html-old/script.js diff --git a/html/style.css b/html-old/style.css similarity index 100% rename from html/style.css rename to html-old/style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..a152394 --- /dev/null +++ b/index.html @@ -0,0 +1,35 @@ + + + + + + + railwayka.ru + + + +
+
Loading...
+
+ + + + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9658e58 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,240 @@ +use wasm_bindgen::prelude::*; +use web_sys::{console, Document, Element, HtmlElement, Window}; + +#[wasm_bindgen(start)] +pub fn main() -> Result<(), JsValue> { + console::log_1(&"🚀 railwayka.ru WASM landing page initializing...".into()); + + let window = web_sys::window().expect("no global window exists"); + let document = window.document().expect("should have a document on window"); + + // Clear body and build page + if let Some(body) = document.body() { + body.set_inner_html(""); + build_page(&document, &body)?; + } + + console::log_1(&"✅ Landing page ready!".into()); + Ok(()) +} + +fn build_page(document: &Document, body: &HtmlElement) -> Result<(), JsValue> { + // Hero Section + let hero = create_hero(document)?; + body.append_child(&hero)?; + + // Services Section + let services = create_services_section(document)?; + body.append_child(&services)?; + + // Tech Stack Section + let tech = create_tech_section(document)?; + body.append_child(&tech)?; + + // Footer + let footer = create_footer(document)?; + body.append_child(&footer)?; + + Ok(()) +} + +fn create_hero(document: &Document) -> Result { + let section = document.create_element("section")?; + section.set_class_name("hero"); + + let container = document.create_element("div")?; + container.set_class_name("container"); + + let title = document.create_element("h1")?; + title.set_text_content(Some("railwayka.ru")); + title.set_class_name("hero-title"); + + let subtitle = document.create_element("p")?; + subtitle.set_text_content(Some("Modern self-hosted infrastructure powered by GitOps")); + subtitle.set_class_name("hero-subtitle"); + + let badge = document.create_element("div")?; + badge.set_class_name("hero-badge"); + badge.set_text_content(Some("🦀 Powered by Rust + WebAssembly")); + + container.append_child(&title)?; + container.append_child(&subtitle)?; + container.append_child(&badge)?; + section.append_child(&container)?; + + Ok(section) +} + +fn create_services_section(document: &Document) -> Result { + let section = document.create_element("section")?; + section.set_class_name("services"); + + let container = document.create_element("div")?; + container.set_class_name("container"); + + let heading = document.create_element("h2")?; + heading.set_text_content(Some("Services")); + heading.set_class_name("section-title"); + container.append_child(&heading)?; + + let grid = document.create_element("div")?; + grid.set_class_name("services-grid"); + + // Live services + let services_live = vec![ + ("Git Repository", "Gitea - Self-hosted Git server with web UI", "🔴", "https://git.dc.railwayka.ru"), + ("CI/CD", "Woodpecker CI - Automated builds and deployments", "🔴", "https://cicd.dc.railwayka.ru"), + ("Container Management", "Portainer - Docker management interface", "🔴", "https://port.dc.railwayka.ru"), + ]; + + for (name, desc, status, url) in services_live { + let card = create_service_card(document, name, desc, status, Some(url))?; + grid.append_child(&card)?; + } + + // Planned services + let services_planned = vec![ + ("Matrix Server", "Decentralized messaging and communication", "🔵"), + ("Blog", "Technical writing and knowledge sharing", "🔵"), + ("Password Manager", "Vaultwarden - Self-hosted password vault", "🔵"), + ("Cloud Storage", "Nextcloud - File sync and sharing", "🔵"), + ("Monitoring", "Grafana + Prometheus - Infrastructure monitoring", "🔵"), + ]; + + for (name, desc, status) in services_planned { + let card = create_service_card(document, name, desc, status, None)?; + grid.append_child(&card)?; + } + + container.append_child(&grid)?; + section.append_child(&container)?; + + Ok(section) +} + +fn create_service_card( + document: &Document, + name: &str, + description: &str, + status: &str, + url: Option<&str>, +) -> Result { + let card = if let Some(link) = url { + let a = document.create_element("a")?; + a.set_attribute("href", link)?; + a.set_attribute("target", "_blank")?; + a.set_attribute("rel", "noopener noreferrer")?; + a + } else { + document.create_element("div")? + }; + card.set_class_name("service-card"); + + let header = document.create_element("div")?; + header.set_class_name("service-header"); + + let title = document.create_element("h3")?; + title.set_text_content(Some(name)); + + let badge = document.create_element("span")?; + badge.set_class_name("status-badge"); + badge.set_text_content(Some(status)); + + header.append_child(&title)?; + header.append_child(&badge)?; + + let desc = document.create_element("p")?; + desc.set_text_content(Some(description)); + desc.set_class_name("service-description"); + + card.append_child(&header)?; + card.append_child(&desc)?; + + Ok(card) +} + +fn create_tech_section(document: &Document) -> Result { + let section = document.create_element("section")?; + section.set_class_name("tech"); + + let container = document.create_element("div")?; + container.set_class_name("container"); + + let heading = document.create_element("h2")?; + heading.set_text_content(Some("Technology Stack")); + heading.set_class_name("section-title"); + container.append_child(&heading)?; + + let grid = document.create_element("div")?; + grid.set_class_name("tech-grid"); + + let technologies = vec![ + ("Dcape", "GitOps platform for containerized applications"), + ("Docker", "Containerization and orchestration"), + ("Traefik", "Automatic HTTPS and reverse proxy"), + ("PowerDNS", "Authoritative DNS server"), + ("PostgreSQL", "Shared database infrastructure"), + ("Rust + WASM", "High-performance web applications"), + ]; + + for (name, desc) in technologies { + let item = create_tech_item(document, name, desc)?; + grid.append_child(&item)?; + } + + container.append_child(&grid)?; + section.append_child(&container)?; + + Ok(section) +} + +fn create_tech_item(document: &Document, name: &str, description: &str) -> Result { + let item = document.create_element("div")?; + item.set_class_name("tech-item"); + + let title = document.create_element("h3")?; + title.set_text_content(Some(name)); + + let desc = document.create_element("p")?; + desc.set_text_content(Some(description)); + + item.append_child(&title)?; + item.append_child(&desc)?; + + Ok(item) +} + +fn create_footer(document: &Document) -> Result { + let footer = document.create_element("footer")?; + footer.set_class_name("footer"); + + let container = document.create_element("div")?; + container.set_class_name("container"); + + let text = document.create_element("p")?; + text.set_inner_html("Built with 🦀 Rust + WebAssembly • Deployed via GitOps"); + + let links = document.create_element("div")?; + links.set_class_name("footer-links"); + + let link_data = vec![ + ("Traefik Dashboard", "https://dc.railwayka.ru/dashboard/"), + ("Gitea", "https://git.dc.railwayka.ru"), + ("Woodpecker CI", "https://cicd.dc.railwayka.ru"), + ]; + + for (name, url) in link_data { + let link = document.create_element("a")?; + link.set_attribute("href", url)?; + link.set_attribute("target", "_blank")?; + link.set_attribute("rel", "noopener noreferrer")?; + link.set_text_content(Some(name)); + links.append_child(&link)?; + } + + container.append_child(&text)?; + container.append_child(&links)?; + footer.append_child(&container)?; + + Ok(footer) +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..0f8af89 --- /dev/null +++ b/style.css @@ -0,0 +1,299 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-dark: #0a0e17; + --bg-card: #1a1f2e; + --bg-card-hover: #252a3a; + --text-primary: #e0e6ed; + --text-secondary: #9ca3af; + --accent: #3b82f6; + --accent-hover: #2563eb; + --rust: #f74c00; + --border: #2d3748; + --success: #10b981; + --planned: #6366f1; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif; + background: var(--bg-dark); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +/* Loading screen */ +#loading { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--bg-dark); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loader { + font-size: 1.2rem; + color: var(--text-secondary); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +.error { + padding: 2rem; + text-align: center; + color: var(--rust); +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; +} + +/* Hero Section */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%); + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%); +} + +.hero .container { + position: relative; + z-index: 1; +} + +.hero-title { + font-size: clamp(3rem, 8vw, 6rem); + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(135deg, var(--accent) 0%, var(--planned) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: clamp(1.2rem, 3vw, 1.5rem); + color: var(--text-secondary); + margin-bottom: 2rem; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.hero-badge { + display: inline-block; + padding: 0.75rem 1.5rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 2rem; + font-size: 1rem; + color: var(--text-primary); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +/* Section Styles */ +section { + padding: 5rem 0; +} + +.section-title { + font-size: 2.5rem; + margin-bottom: 3rem; + text-align: center; + color: var(--text-primary); +} + +/* Services Section */ +.services { + background: var(--bg-dark); +} + +.services-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.service-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.3s ease; + text-decoration: none; + color: inherit; + display: block; +} + +.service-card:hover { + background: var(--bg-card-hover); + border-color: var(--accent); + transform: translateY(-4px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2); +} + +.service-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.service-card h3 { + font-size: 1.25rem; + color: var(--text-primary); + flex: 1; +} + +.status-badge { + font-size: 1.5rem; + flex-shrink: 0; + margin-left: 0.5rem; +} + +.service-description { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.6; +} + +/* Tech Section */ +.tech { + background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%); +} + +.tech-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.tech-item { + text-align: center; + padding: 2rem 1rem; +} + +.tech-item h3 { + font-size: 1.5rem; + margin-bottom: 0.75rem; + color: var(--accent); +} + +.tech-item p { + color: var(--text-secondary); + font-size: 0.95rem; +} + +/* Footer */ +.footer { + background: var(--bg-card); + border-top: 1px solid var(--border); + padding: 3rem 0; + text-align: center; +} + +.footer p { + color: var(--text-secondary); + margin-bottom: 1.5rem; +} + +.rust-love { + color: var(--rust); + font-weight: 600; +} + +.footer-links { + display: flex; + justify-content: center; + gap: 2rem; + flex-wrap: wrap; +} + +.footer-links a { + color: var(--accent); + text-decoration: none; + transition: color 0.3s ease; + font-size: 0.95rem; +} + +.footer-links a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + padding: 0 1.5rem; + } + + section { + padding: 3rem 0; + } + + .services-grid, + .tech-grid { + grid-template-columns: 1fr; + } + + .hero { + min-height: 80vh; + } + + .footer-links { + flex-direction: column; + gap: 1rem; + } +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Selection color */ +::selection { + background: var(--accent); + color: white; +} + +::-moz-selection { + background: var(--accent); + color: white; +}