diff --git a/index.js b/index.js index 2d37e90..33d38fd 100644 --- a/index.js +++ b/index.js @@ -52,8 +52,10 @@ const wsMessageRateLimiter = new RateLimiterMemory({ duration: 1, }); +const path = require('path'); const app = express(); const port = 3000; +app.use(express.static(path.join(__dirname, 'public'))); app.use(json()); // Rate limiting middleware for HTTP requests @@ -646,9 +648,6 @@ async function authenticateToken(req, res, next) { } } -app.get('/', (req, res) => { - res.send('Hello World!'); -}); app.post('/login', async (req, res) => { const { email, password } = req.body; diff --git a/public/app/index.html b/public/app/index.html new file mode 100644 index 0000000..77962f3 --- /dev/null +++ b/public/app/index.html @@ -0,0 +1,260 @@ + + + + + + BlindMaster App — Coming Soon + + + + + + +
+ + +
+ + In Development +
+ +

The web app is
coming soon.

+ +

+ We're building the BlindMaster web experience so you can control + your blinds from any browser — no install required. Stay tuned. +

+ +
+
+
+
Development in progress
+ + + + + + Back to homepage + +
+ + + + diff --git a/public/css/styles.css b/public/css/styles.css new file mode 100644 index 0000000..ccdb0a2 --- /dev/null +++ b/public/css/styles.css @@ -0,0 +1,934 @@ +/* ============================================================ + BlindMaster Landing Page — Styles + Theme: time-based accent (orange / blue / purple) + matching the Flutter app's time-of-day color system + ============================================================ */ + +/* ---------- CSS Custom Properties (set by JS at runtime) --- */ +:root { + --accent: #3B82F6; /* default: day blue */ + --accent-rgb: 59, 130, 246; + --accent-dark: #1D4ED8; + --accent-glow: rgba(59, 130, 246, 0.25); + + --bg: #0A0B0F; + --bg-surface: #12141A; + --bg-card: #181B24; + --bg-card-border: rgba(255,255,255,0.07); + + --text: #F1F5F9; + --text-muted: #8892A4; + --text-faint: #4B5563; + + --slat-color: #6B4C2A; + --slat-shadow: rgba(0,0,0,0.4); + + --transition: 0.6s cubic-bezier(0.16, 1, 0.3, 1); + --radius: 16px; + --radius-sm: 10px; +} + +/* ---------- Reset & Base ----------------------------------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; font-size: 16px; } + +body { + font-family: 'Poppins', sans-serif; + background: var(--bg); + color: var(--text); + overflow-x: hidden; + line-height: 1.6; +} + +a { color: inherit; text-decoration: none; } +img { display: block; max-width: 100%; } +button { font-family: inherit; cursor: pointer; border: none; } + +/* ---------- Utility ---------------------------------------- */ +.section-inner { + max-width: 1160px; + margin: 0 auto; + padding: 0 24px; +} + +.section-eyebrow { + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 12px; +} + +.section-title { + font-size: clamp(1.8rem, 4vw, 2.8rem); + font-weight: 700; + line-height: 1.2; + margin-bottom: 16px; +} + +.section-sub { + font-size: 1.05rem; + color: var(--text-muted); + max-width: 560px; +} + +.section-header { margin-bottom: 56px; } + +/* ---------- Buttons ---------------------------------------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 13px 28px; + border-radius: 100px; + font-size: 0.95rem; + font-weight: 600; + transition: all 0.25s ease; + white-space: nowrap; +} + +.btn-primary { + background: var(--accent); + color: #fff; + box-shadow: 0 0 24px var(--accent-glow); +} +.btn-primary:hover { + filter: brightness(1.12); + box-shadow: 0 0 36px var(--accent-glow); + transform: translateY(-1px); +} + +.btn-ghost { + background: transparent; + color: var(--text-muted); + border: 1.5px solid var(--bg-card-border); +} +.btn-ghost:hover { + color: var(--text); + border-color: rgba(255,255,255,0.18); + background: rgba(255,255,255,0.04); +} + +/* ---------- Scroll reveal ---------------------------------- */ +.reveal { + opacity: 0; + transform: translateY(28px); + transition: opacity 0.7s ease, transform 0.7s ease; + transition-delay: var(--delay, 0ms); +} +.reveal.visible { + opacity: 1; + transform: translateY(0); +} + +/* ============================================================ + NAV + ============================================================ */ +.nav { + position: fixed; + top: 0; left: 0; right: 0; + z-index: 100; + padding: 16px 0; + transition: background 0.3s, backdrop-filter 0.3s, box-shadow 0.3s; +} +.nav.scrolled { + background: rgba(10, 11, 15, 0.85); + backdrop-filter: blur(16px); + box-shadow: 0 1px 0 rgba(255,255,255,0.06); +} +.nav-inner { + max-width: 1160px; + margin: 0 auto; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; +} +.nav-logo { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + font-size: 1.05rem; +} +.nav-logo-icon { + width: 26px; + height: 22px; + display: flex; + flex-direction: column; + gap: 4px; + justify-content: center; +} +.nav-logo-icon.small { width: 20px; height: 18px; gap: 3px; } +.slat { + height: 3px; + background: var(--accent); + border-radius: 2px; + transition: background var(--transition); +} +.nav-links { + display: flex; + align-items: center; + gap: 32px; + list-style: none; +} +.nav-links a { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-muted); + transition: color 0.2s; +} +.nav-links a:hover { color: var(--text); } +.nav-cta { + background: var(--accent) !important; + color: #fff !important; + padding: 8px 20px; + border-radius: 100px; + font-size: 0.85rem !important; + transition: filter 0.2s, box-shadow 0.2s !important; +} +.nav-cta:hover { filter: brightness(1.1); } +.nav-launch { + background: transparent; + color: var(--text) !important; + border: 1.5px solid var(--bg-card-border); + padding: 7px 18px; + border-radius: 100px; + font-size: 0.85rem !important; + transition: border-color 0.2s, background 0.2s !important; +} +.nav-launch:hover { + border-color: rgba(255,255,255,0.2) !important; + background: rgba(255,255,255,0.05) !important; +} +.mobile-nav-launch { + color: var(--accent) !important; + font-weight: 600 !important; +} +.nav-hamburger { + display: none; + flex-direction: column; + gap: 5px; + background: transparent; + padding: 4px; +} +.nav-hamburger span { + display: block; + width: 22px; + height: 2px; + background: var(--text-muted); + border-radius: 2px; + transition: 0.3s; +} + +.mobile-nav { + display: none; + position: fixed; + top: 64px; left: 0; right: 0; + background: rgba(10,11,15,0.97); + backdrop-filter: blur(16px); + border-bottom: 1px solid var(--bg-card-border); + z-index: 99; + padding: 16px 0; + transform: translateY(-16px); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s, transform 0.25s; +} +.mobile-nav.open { + transform: translateY(0); + opacity: 1; + pointer-events: all; + display: block; +} +.mobile-nav ul { list-style: none; } +.mobile-nav-link { + display: block; + padding: 14px 24px; + font-size: 1rem; + font-weight: 500; + color: var(--text-muted); +} +.mobile-nav-link:hover { color: var(--text); } + +/* ============================================================ + HERO + ============================================================ */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + padding: 120px 24px 80px; + max-width: 1160px; + margin: 0 auto; + gap: 48px; + position: relative; +} +.hero-bg-grid { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: + linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, black 30%, transparent 100%); +} +.hero-content { + flex: 1; + min-width: 0; + z-index: 1; +} +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--accent); + background: rgba(var(--accent-rgb), 0.12); + border: 1px solid rgba(var(--accent-rgb), 0.25); + padding: 6px 14px; + border-radius: 100px; + margin-bottom: 28px; +} +.badge-dot { + width: 6px; height: 6px; + background: var(--accent); + border-radius: 50%; + animation: pulse 2s infinite; +} +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} +.hero-title { + font-size: clamp(2.4rem, 6vw, 4.2rem); + font-weight: 800; + line-height: 1.1; + margin-bottom: 20px; + letter-spacing: -0.02em; +} +.hero-accent { + background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #fff)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} +.hero-subtitle { + font-size: 1.1rem; + color: var(--text-muted); + max-width: 500px; + margin-bottom: 36px; + line-height: 1.7; +} +.hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 48px; +} +.hero-stats { + display: flex; + align-items: center; + gap: 24px; +} +.stat { text-align: center; } +.stat-num { + display: block; + font-size: 1.6rem; + font-weight: 700; + color: var(--text); + line-height: 1; +} +.stat-label { + font-size: 0.72rem; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-top: 4px; + display: block; +} +.stat-divider { + width: 1px; + height: 36px; + background: var(--bg-card-border); +} + +/* ---- Hero visual: window + blind ---- */ +.hero-visual { + flex: 0 0 420px; + z-index: 1; +} +.window-frame { + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + border-radius: 20px; + overflow: hidden; + box-shadow: 0 32px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset; +} +.window-header { + display: flex; + align-items: center; + gap: 6px; + padding: 12px 16px; + background: rgba(255,255,255,0.03); + border-bottom: 1px solid var(--bg-card-border); +} +.window-dot { + width: 12px; height: 12px; + border-radius: 50%; +} +.window-dot.red { background: #FF5F57; } +.window-dot.yellow { background: #FEBC2E; } +.window-dot.green { background: #28C840; } +.window-title { + font-size: 0.78rem; + color: var(--text-muted); + margin-left: 8px; + font-weight: 500; +} +.blind-container { + height: 220px; + background: linear-gradient(180deg, #87CEEB 0%, #B0D4F1 100%); + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 4px 0; +} +.blind-slat { + height: 18px; + background: var(--slat-color); + border-radius: 2px; + margin: 0 12px; + box-shadow: 0 2px 4px var(--slat-shadow); + transform-origin: center; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + transform: rotateX(calc(var(--i, 0) * 0deg)); +} +.app-ui-overlay { + padding: 16px 20px; +} +.app-slider-label { + font-size: 0.72rem; + color: var(--text-muted); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 10px; +} +.app-slider-track { + height: 6px; + background: rgba(255,255,255,0.08); + border-radius: 100px; + position: relative; + margin-bottom: 8px; +} +.app-slider-thumb { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + background: var(--accent); + border-radius: 50%; + box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.3); + transition: left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), background var(--transition); + cursor: pointer; +} +.app-slider-vals { + display: flex; + justify-content: space-between; + font-size: 0.65rem; + color: var(--text-faint); +} + +/* ============================================================ + FEATURES + ============================================================ */ +.features { + padding: 120px 0; + background: linear-gradient(180deg, var(--bg) 0%, var(--bg-surface) 100%); +} +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} +.feature-card { + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius); + padding: 28px; + transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s; +} +.feature-card:hover { + border-color: rgba(var(--accent-rgb), 0.35); + box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 0 1px rgba(var(--accent-rgb), 0.1) inset; + transform: translateY(-3px); +} +.feature-icon { + width: 48px; height: 48px; + background: rgba(var(--accent-rgb), 0.12); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 18px; + color: var(--accent); + transition: background var(--transition), color var(--transition); +} +.feature-icon svg { width: 22px; height: 22px; } +.feature-card h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 10px; +} +.feature-card p { + font-size: 0.88rem; + color: var(--text-muted); + line-height: 1.65; +} + +/* ============================================================ + HOW IT WORKS + ============================================================ */ +.how-it-works { + padding: 120px 0; + background: var(--bg-surface); +} + +/* Arch flow */ +.arch-flow { + display: flex; + align-items: center; + gap: 0; + margin-bottom: 24px; + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 8px; +} +.arch-node { + flex: 1; + min-width: 180px; + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius); + padding: 24px 20px; + text-align: center; + transition: border-color 0.25s; +} +.arch-node-center { + border-color: rgba(var(--accent-rgb), 0.35); + box-shadow: 0 0 40px rgba(var(--accent-rgb), 0.08); +} +.arch-icon { + width: 52px; height: 52px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 14px; + color: var(--accent); +} +.app-icon { background: rgba(var(--accent-rgb), 0.12); } +.server-icon { background: rgba(var(--accent-rgb), 0.18); } +.device-icon { background: rgba(var(--accent-rgb), 0.12); } +.arch-icon svg { width: 24px; height: 24px; } +.arch-node h4 { font-size: 0.95rem; font-weight: 600; margin-bottom: 8px; } +.arch-node p { font-size: 0.78rem; color: var(--text-muted); line-height: 1.6; } +.arch-tag { + margin-top: 12px; + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--accent); + background: rgba(var(--accent-rgb), 0.1); + padding: 3px 10px; + border-radius: 100px; + display: inline-block; + font-family: 'Courier New', monospace; +} +.arch-arrow { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 0 8px; + flex-shrink: 0; +} +.arch-arrow-line { + width: 40px; + height: 1px; + background: linear-gradient(90deg, var(--bg-card-border), rgba(var(--accent-rgb), 0.4)); +} +.arch-arrow-label { + font-size: 0.6rem; + color: var(--text-faint); + white-space: nowrap; + letter-spacing: 0.04em; + writing-mode: horizontal-tb; +} +.arch-ble-note { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.78rem; + color: var(--text-faint); + margin-bottom: 56px; + padding: 10px 16px; + background: rgba(255,255,255,0.03); + border-radius: var(--radius-sm); + border: 1px solid var(--bg-card-border); + color: var(--text-muted); +} + +/* Steps */ +.steps-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 20px; +} +.step { + padding: 24px; + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius); +} +.step-num { + font-size: 2.2rem; + font-weight: 800; + color: rgba(var(--accent-rgb), 0.2); + line-height: 1; + margin-bottom: 12px; + font-variant-numeric: tabular-nums; +} +.step h4 { font-size: 0.95rem; font-weight: 600; margin-bottom: 10px; } +.step p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.65; } + +/* ============================================================ + TECH / OPEN HARDWARE + ============================================================ */ +.tech { + padding: 120px 0; + background: linear-gradient(180deg, var(--bg-surface) 0%, var(--bg) 100%); +} +.tech-content { + display: flex; + align-items: center; + gap: 64px; + flex-wrap: wrap; +} +.tech-text { flex: 1; min-width: 280px; } +.tech-text .section-title { margin-bottom: 20px; } +.tech-text p { + font-size: 0.95rem; + color: var(--text-muted); + line-height: 1.75; + margin-bottom: 28px; +} +.tech-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 12px; +} +.tech-list li { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 0.88rem; + color: var(--text-muted); + line-height: 1.5; +} +.tech-check { + color: var(--accent); + font-weight: 700; + flex-shrink: 0; + margin-top: 1px; +} + +/* Chip visual */ +.tech-visual { + flex: 0 0 280px; + display: flex; + justify-content: center; +} +.chip-card { + position: relative; + width: 240px; +} +.chip-glow { + position: absolute; + inset: -40px; + background: radial-gradient(ellipse at 50% 50%, rgba(var(--accent-rgb), 0.18) 0%, transparent 70%); + pointer-events: none; + transition: background var(--transition); +} +.chip-body { + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + border-radius: 20px; + padding: 24px 20px; + text-align: center; + position: relative; +} +.chip-label { + font-size: 1.1rem; + font-weight: 700; + margin-bottom: 4px; +} +.chip-sub { + font-size: 0.7rem; + color: var(--text-faint); + letter-spacing: 0.06em; + margin-bottom: 20px; +} +.chip-pins { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 20px; +} +.chip-pin-row { + display: flex; + flex-direction: column; + gap: 6px; +} +.chip-pin { + width: 24px; + height: 7px; + background: rgba(var(--accent-rgb), 0.35); + border-radius: 2px; + transition: background var(--transition); +} +.chip-core { + flex: 1; + aspect-ratio: 1; + background: rgba(var(--accent-rgb), 0.08); + border: 1px solid rgba(var(--accent-rgb), 0.2); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + max-width: 100px; + max-height: 100px; +} +.chip-core-inner { + width: 60%; + height: 60%; + background: rgba(var(--accent-rgb), 0.15); + border-radius: 6px; + border: 1px solid rgba(var(--accent-rgb), 0.3); +} +.chip-tags { + display: flex; + gap: 6px; + flex-wrap: wrap; + justify-content: center; +} +.chip-tags span { + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--accent); + background: rgba(var(--accent-rgb), 0.1); + padding: 3px 8px; + border-radius: 100px; + font-family: 'Courier New', monospace; +} + +/* ============================================================ + COMING SOON + ============================================================ */ +.coming-soon { + padding: 120px 0; + background: var(--bg); +} +.coming-soon-card { + background: var(--bg-card); + border: 1px solid rgba(var(--accent-rgb), 0.2); + border-radius: 24px; + padding: 64px 56px; + text-align: center; + position: relative; + overflow: hidden; + box-shadow: 0 0 80px rgba(var(--accent-rgb), 0.06); +} +.coming-soon-card::before { + content: ''; + position: absolute; + top: -80px; left: 50%; + transform: translateX(-50%); + width: 400px; height: 200px; + background: radial-gradient(ellipse, rgba(var(--accent-rgb), 0.15) 0%, transparent 70%); + pointer-events: none; +} +.cs-badge { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--accent); + background: rgba(var(--accent-rgb), 0.12); + border: 1px solid rgba(var(--accent-rgb), 0.25); + padding: 6px 16px; + border-radius: 100px; + margin-bottom: 28px; +} +.cs-badge-dot { + width: 6px; height: 6px; + background: var(--accent); + border-radius: 50%; + animation: pulse 1.5s infinite; +} +.coming-soon-card h2 { + font-size: clamp(1.6rem, 3.5vw, 2.4rem); + font-weight: 700; + margin-bottom: 16px; + line-height: 1.2; +} +.coming-soon-card > p { + font-size: 0.95rem; + color: var(--text-muted); + max-width: 520px; + margin: 0 auto 36px; + line-height: 1.7; +} +.coming-soon-card strong { color: var(--text); } +.cs-highlights { + display: flex; + justify-content: center; + gap: 24px; + flex-wrap: wrap; + margin-bottom: 40px; +} +.cs-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + color: var(--text-muted); + font-weight: 500; +} +.cs-item svg { color: var(--accent); flex-shrink: 0; } +.cs-form { + display: flex; + gap: 10px; + max-width: 420px; + margin: 0 auto 16px; + flex-wrap: wrap; + justify-content: center; +} +.cs-input { + flex: 1; + min-width: 200px; + background: rgba(255,255,255,0.05); + border: 1.5px solid var(--bg-card-border); + border-radius: 100px; + padding: 13px 20px; + font-family: inherit; + font-size: 0.9rem; + color: var(--text); + outline: none; + transition: border-color 0.2s; +} +.cs-input::placeholder { color: var(--text-faint); } +.cs-input:focus { border-color: var(--accent); } +.cs-btn { flex-shrink: 0; } +.cs-disclaimer { + font-size: 0.72rem; + color: var(--text-faint); + margin-top: 4px; +} +.cs-success { + color: #4ADE80; + font-size: 0.9rem; + font-weight: 500; + margin-top: 12px; +} + +/* ============================================================ + FOOTER + ============================================================ */ +.footer { + border-top: 1px solid var(--bg-card-border); + padding: 48px 24px; +} +.footer-inner { + max-width: 1160px; + margin: 0 auto; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} +.footer-logo { + display: flex; + align-items: center; + gap: 8px; + font-weight: 700; + font-size: 0.95rem; +} +.footer-tagline { + font-size: 0.82rem; + color: var(--text-faint); +} +.footer-links { + display: flex; + gap: 24px; + flex-wrap: wrap; + justify-content: center; +} +.footer-links a { + font-size: 0.82rem; + color: var(--text-faint); + transition: color 0.2s; +} +.footer-links a:hover { color: var(--text-muted); } +.footer-copy { + font-size: 0.72rem; + color: var(--text-faint); +} + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 900px) { + .hero { + flex-direction: column; + padding-top: 100px; + text-align: center; + } + .hero-subtitle { margin-left: auto; margin-right: auto; } + .hero-actions { justify-content: center; } + .hero-stats { justify-content: center; } + .hero-visual { width: 100%; flex: none; } + .window-frame { max-width: 380px; margin: 0 auto; } + .nav-links { display: none; } + .nav-hamburger { display: flex; } + .tech-content { flex-direction: column; } + .tech-visual { flex: none; width: 100%; } + .arch-flow { gap: 4px; } + .arch-arrow-line { width: 24px; } +} + +@media (max-width: 600px) { + .coming-soon-card { padding: 40px 24px; } + .arch-flow { flex-direction: column; } + .arch-arrow { flex-direction: row; gap: 8px; } + .arch-arrow-line { width: 20px; height: 1px; } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..39e3f08 --- /dev/null +++ b/public/index.html @@ -0,0 +1,419 @@ + + + + + + BlindMaster — Smart Blinds, Effortlessly Controlled + + + + + + + + + + + +
+ +
+ + +
+
+
+
+ + Smart Home IoT +
+

+ Your blinds,
+ on your schedule. +

+

+ BlindMaster brings real-time remote control, intelligent scheduling, and + seamless IoT integration to your motorized window blinds — from anywhere. +

+ +
+
+ 11 + Positions +
+
+
+ + Schedules +
+
+
+ Real-time + Socket.IO Sync +
+
+
+
+
+
+
+
+
+ BlindMaster +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Position
+
+
+
+
+ Close ↓ + Open + Close ↑ +
+
+
+
+
+ + +
+
+
+

Why BlindMaster

+

Everything you need to control your light.

+
+
+ +
+
+ + + + +
+

Real-Time Control

+

Adjust any blind instantly via WebSocket — sub-second response from your phone to your window, anywhere in the world.

+
+ +
+
+ + + + + +
+

Smart Scheduling

+

Set automated cron-based schedules per blind or per group. Wake up to light gradually filling the room — automatically.

+
+ +
+
+ + + + + +
+

Groups & Rooms

+

Group multiple blinds and control them together. One tap to raise every blind in a room simultaneously.

+
+ +
+
+ + + +
+

Secure by Default

+

JWT authentication, Argon2 password hashing, TLS everywhere, and multi-layer rate limiting — your home stays yours.

+
+ +
+
+ + + + +
+

Auto-Calibration

+

A guided multi-stage calibration flow maps encoder ticks to your exact blind travel — precise positioning every time.

+
+ +
+
+ + + + + + +
+

Battery-Aware

+

Built-in MAX17048 fuel gauge monitoring with low-battery alerts. Dynamic CPU scaling and servo power gating extend runtime.

+
+ +
+
+
+ + +
+
+
+

Architecture

+

Three components. One seamless system.

+

BlindMaster is a full-stack IoT platform — every layer is purpose-built to work together.

+
+
+
+
+ + + + +
+

Flutter App

+

iOS & Android mobile app. Time-based theming. Real-time slider control. Schedule management.

+
blinds_flutter
+
+
+
+
Socket.IO + JWT + TLS
+
+
+
+
+ + + + + + +
+

Express Server

+

Central relay & API. PostgreSQL + MongoDB. Agenda scheduling. Email via AWS SES.

+
blinds_express
+
+
+
+
Socket.IO + JWT + TLS
+
+
+
+
+ + + +
+

ESP32-C6

+

FreeRTOS firmware. BLE provisioning. Servo + encoder control. NVS persistent state.

+
Blinds_XIAO
+
+
+
+ + + + BLE provisioning (setup only) — phone pairs directly to device to deliver Wi-Fi credentials & auth token +
+
+
+
01
+

Provision Once

+

Pair your ESP32-C6 device over BLE from the app. Enter your Wi-Fi credentials and authenticate — stored securely on the device in NVS.

+
+
+
02
+

Calibrate

+

A guided handshake walks the device through measuring your blind's full travel range. Precise 11-position control from that point on.

+
+
+
03
+

Control & Schedule

+

Use the app slider for immediate control or set recurring schedules. Changes reach your blinds in real-time over Socket.IO.

+
+
+
+
+ + +
+
+
+
+

Open Hardware

+

Built on the Seeed XIAO ESP32-C6.

+

BlindMaster hardware is built around the Seeed XIAO ESP32-C6 — a compact, powerful RISC-V module with built-in Wi-Fi 6 and Bluetooth 5.3. The firmware is open-source ESP-IDF, and hardware schematics will be open-sourced at launch.

+
    +
  • + + LEDC PWM servo control at 50Hz with encoder-based position feedback +
  • +
  • + + MAX17048 I²C fuel gauge for accurate LiPo state-of-charge monitoring +
  • +
  • + + Dynamic CPU scaling (80–160MHz) + light sleep for extended battery life +
  • +
  • + + NimBLE secure provisioning — no cloud dependency during setup +
  • +
+
+
+
+
+
+
ESP32-C6
+
RISC-V · Wi-Fi 6 · BT 5.3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FreeRTOS + ESP-IDF + NimBLE +
+
+
+
+
+
+
+ + +
+
+
+
+ + Coming Soon +
+

Ready to take control of your light?

+

+ BlindMaster is launching on TestFlight for iOS beta testers, + with hardware schematics open-sourced at the same time. Join the early access + list to be first to know. +

+
+
+ + iOS TestFlight Beta +
+
+ + Open-Source Hardware +
+
+ + Android App (following) +
+
+
+ + +
+

No spam. Just a launch notification.

+
+
+
+ + + + + + + diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..50ef28f --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,201 @@ +/* ============================================================ + BlindMaster Landing Page — main.js + - Time-based accent color (mirrors Flutter app theming) + - Nav scroll behavior + - Scroll reveal animations + - Hero blind animation + - Interactive demo slider + - Early access form + ============================================================ */ + +(function () { + 'use strict'; + + // ---------------------------------------------------------- + // 1. Time-based theme (matches Flutter app color system) + // Orange 5–10am | Blue 10am–6pm | Purple 6pm+ + // ---------------------------------------------------------- + function getThemeForHour(hour) { + if (hour >= 5 && hour < 10) { + return { + name: 'morning', + accent: '#F97316', + accentRgb: '249, 115, 22', + accentDark: '#C2410C', + accentGlow: 'rgba(249, 115, 22, 0.25)', + }; + } else if (hour >= 10 && hour < 18) { + return { + name: 'day', + accent: '#3B82F6', + accentRgb: '59, 130, 246', + accentDark: '#1D4ED8', + accentGlow: 'rgba(59, 130, 246, 0.25)', + }; + } else { + return { + name: 'evening', + accent: '#7C3AED', + accentRgb: '124, 58, 237', + accentDark: '#5B21B6', + accentGlow: 'rgba(124, 58, 237, 0.25)', + }; + } + } + + function applyTheme(theme) { + const root = document.documentElement; + root.style.setProperty('--accent', theme.accent); + root.style.setProperty('--accent-rgb', theme.accentRgb); + root.style.setProperty('--accent-dark', theme.accentDark); + root.style.setProperty('--accent-glow', theme.accentGlow); + } + + const hour = new Date().getHours(); + applyTheme(getThemeForHour(hour)); + + // ---------------------------------------------------------- + // 2. Nav — scrolled state + // ---------------------------------------------------------- + const nav = document.getElementById('nav'); + function updateNav() { + if (window.scrollY > 40) { + nav.classList.add('scrolled'); + } else { + nav.classList.remove('scrolled'); + } + } + window.addEventListener('scroll', updateNav, { passive: true }); + updateNav(); + + // ---------------------------------------------------------- + // 3. Mobile nav toggle + // ---------------------------------------------------------- + const hamburger = document.getElementById('hamburger'); + const mobileNav = document.getElementById('mobileNav'); + let mobileOpen = false; + + hamburger.addEventListener('click', () => { + mobileOpen = !mobileOpen; + mobileNav.classList.toggle('open', mobileOpen); + }); + + document.querySelectorAll('.mobile-nav-link').forEach(link => { + link.addEventListener('click', () => { + mobileOpen = false; + mobileNav.classList.remove('open'); + }); + }); + + // ---------------------------------------------------------- + // 4. Scroll reveal + // ---------------------------------------------------------- + const revealEls = document.querySelectorAll('.reveal'); + const revealObserver = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + revealObserver.unobserve(entry.target); + } + }); + }, + { threshold: 0.12, rootMargin: '0px 0px -40px 0px' } + ); + revealEls.forEach(el => revealObserver.observe(el)); + + // ---------------------------------------------------------- + // 5. Hero blind animation + // Cycle through positions to demo the 0–10 position model + // ---------------------------------------------------------- + const slats = document.querySelectorAll('.blind-slat'); + const sliderThumb = document.getElementById('sliderThumb'); + + // Position 0–10: map to slat rotation angle + // 0 = fully closed (slats flat, blocking light) + // 5 = fully open (slats perpendicular, maximum light) + // 10 = closed from opposite direction + function positionToAngle(pos) { + if (pos <= 5) { + return (pos / 5) * 75; // 0° → 75° + } else { + return 75 - ((pos - 5) / 5) * 75; // 75° → 0° + } + } + + function positionToSliderPercent(pos) { + return (pos / 10) * 100; + } + + function setBlindPosition(pos) { + const angle = positionToAngle(pos); + slats.forEach(slat => { + slat.style.transform = `rotateX(${angle}deg)`; + }); + sliderThumb.style.left = `${positionToSliderPercent(pos)}%`; + } + + // Animate through positions: 0 → 5 → 10 → 5 → 0, looping + const demoPositions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; + let demoIndex = 0; + + setBlindPosition(demoPositions[0]); + + setInterval(() => { + demoIndex = (demoIndex + 1) % demoPositions.length; + setBlindPosition(demoPositions[demoIndex]); + }, 700); + + // ---------------------------------------------------------- + // 6. Interactive slider on demo window (drag/click) + // ---------------------------------------------------------- + const sliderTrack = document.querySelector('.app-slider-track'); + + function handleSliderInteraction(clientX) { + const rect = sliderTrack.getBoundingClientRect(); + const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); + const pos = Math.round(pct * 10); + setBlindPosition(pos); + } + + sliderTrack.addEventListener('mousedown', (e) => { + handleSliderInteraction(e.clientX); + const onMove = (e) => handleSliderInteraction(e.clientX); + const onUp = () => { + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }); + + sliderTrack.addEventListener('touchstart', (e) => { + handleSliderInteraction(e.touches[0].clientX); + const onMove = (e) => handleSliderInteraction(e.touches[0].clientX); + const onEnd = () => { + window.removeEventListener('touchmove', onMove); + window.removeEventListener('touchend', onEnd); + }; + window.addEventListener('touchmove', onMove); + window.addEventListener('touchend', onEnd); + }, { passive: true }); + + // ---------------------------------------------------------- + // 7. Early access form + // ---------------------------------------------------------- + const form = document.getElementById('earlyAccessForm'); + const emailInput = document.getElementById('emailInput'); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + const email = emailInput.value.trim(); + if (!email) return; + + // Replace form with success message + const successMsg = document.createElement('p'); + successMsg.className = 'cs-success'; + successMsg.textContent = `You're on the list! We'll notify ${email} at launch.`; + form.replaceWith(successMsg); + }); + +})();