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
This commit is contained in:
parent
ad2a586de2
commit
15a18e45b4
|
|
@ -0,0 +1,5 @@
|
||||||
|
[build]
|
||||||
|
target = "wasm32-unknown-unknown"
|
||||||
|
|
||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ["-C", "link-arg=-s"]
|
||||||
|
|
@ -1,2 +1,20 @@
|
||||||
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.env.bak
|
.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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -3,4 +3,4 @@ ARG IMAGE_VER=1.27-alpine
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM ${IMAGE}:${IMAGE_VER}
|
FROM --platform=$BUILDPLATFORM ${IMAGE}:${IMAGE_VER}
|
||||||
|
|
||||||
COPY html /usr/share/nginx/html
|
COPY dist /usr/share/nginx/html
|
||||||
|
|
|
||||||
81
README.md
81
README.md
|
|
@ -1,18 +1,22 @@
|
||||||
# railwayka.ru Landing Page
|
# 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
|
## 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
|
- Service cards showcasing current and planned services
|
||||||
- Technology stack descriptions
|
- Technology stack descriptions
|
||||||
- GitOps workflow visualization
|
- Fully responsive layout
|
||||||
- Konami code easter egg
|
- Minimal bundle size (~19KB WASM)
|
||||||
- Responsive design
|
|
||||||
|
|
||||||
## Technology Stack
|
## 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
|
- **Nginx**: Alpine-based web server
|
||||||
- **Docker**: Containerized deployment
|
- **Docker**: Containerized deployment
|
||||||
- **Traefik**: Automatic HTTPS via Let's Encrypt
|
- **Traefik**: Automatic HTTPS via Let's Encrypt
|
||||||
|
|
@ -44,32 +48,75 @@ make docker-build
|
||||||
make up
|
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
|
```bash
|
||||||
# Serve files with any web server
|
# 1. Build Rust to WASM
|
||||||
cd html
|
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
|
python3 -m http.server 8000
|
||||||
# Visit http://localhost: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
|
├── Cargo.toml # Rust project manifest
|
||||||
├── docker-compose.yml # Service definition
|
├── .cargo/
|
||||||
├── Dockerfile # Custom nginx image
|
│ └── config.toml # Rust build configuration
|
||||||
├── .woodpecker.yml # CI/CD pipeline
|
├── src/
|
||||||
├── html/ # Static website files
|
│ └── lib.rs # Main Rust/WASM code
|
||||||
|
├── index.html # HTML shell
|
||||||
|
├── style.css # Styling
|
||||||
|
├── dist/ # Built artifacts (committed to git)
|
||||||
│ ├── index.html
|
│ ├── index.html
|
||||||
│ ├── style.css
|
│ ├── style.css
|
||||||
│ └── script.js
|
│ ├── railwayka_landing.js
|
||||||
└── README.md
|
│ ├── 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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="railwayka.ru - Modern self-hosted infrastructure powered by GitOps">
|
||||||
|
<title>railwayka.ru</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading">
|
||||||
|
<div class="loader">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init from './railwayka_landing.js';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
await init();
|
||||||
|
// Remove loading indicator
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
if (loading) {
|
||||||
|
loading.remove();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize WASM:', error);
|
||||||
|
document.body.innerHTML = '<div class="error">Failed to load application. Please refresh the page.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||||
|
|
@ -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;
|
||||||
Binary file not shown.
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="railwayka.ru - Modern self-hosted infrastructure powered by GitOps">
|
||||||
|
<title>railwayka.ru</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading">
|
||||||
|
<div class="loader">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init from './railwayka_landing.js';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
await init();
|
||||||
|
// Remove loading indicator
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
if (loading) {
|
||||||
|
loading.remove();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize WASM:', error);
|
||||||
|
document.body.innerHTML = '<div class="error">Failed to load application. Please refresh the page.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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<Element, JsValue> {
|
||||||
|
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<Element, JsValue> {
|
||||||
|
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<Element, JsValue> {
|
||||||
|
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<Element, JsValue> {
|
||||||
|
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<Element, JsValue> {
|
||||||
|
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<Element, JsValue> {
|
||||||
|
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 <span class=\"rust-love\">🦀 Rust</span> + 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)
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue