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 0000000..37de7d2 Binary files /dev/null and b/dist/railwayka_landing_bg.wasm differ 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; +}