diff --git a/index.html b/index.html index 1b28d28..d7ef795 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + diff --git a/src/Mansion.jsx b/src/Mansion.jsx index 9fa067c..db3a5d0 100644 --- a/src/Mansion.jsx +++ b/src/Mansion.jsx @@ -2,31 +2,76 @@ import React, { useCallback, useEffect, useState } from 'react'; import { api } from './api.js'; import { copyToast } from './toast.js'; -// amerc Mansion β€” a hub of amerc services. First service: Showcase (reverse proxy). +// amerc Mansion β€” a hall of amerc services. First live service: Showcase (reverse proxy). export default function Mansion({ auth, setRoute }) { if (!auth.ready) return

Loading…

; - if (!auth.user) return ( -

The Mansion

Log in to use amerc services.

- ); + const loggedIn = !!auth.user; + const handle = loggedIn ? auth.user.handle.toLowerCase().replace(/[^a-z0-9]/g, '') : 'you'; return ( -
-

The Mansion

{auth.user.handle}'s services
-

amerc hosts your services behind its edge. Each one lives in a room of the mansion.

-
- -
πŸ—„οΈ
Netdisk relaycoming soon
-
πŸ€–
Hosted webagentcoming soon
+
+
+ +
+

The Mansion

+

A hall of amerc services that put your work on the open internet β€” each lives behind amerc's edge and is brought to life by your own broker. No servers to rent, no ports to forward.

+ {loggedIn + ? {auth.user.handle}'s rooms are open + : } +
+
+ +
+ + +
); } -function Showcase({ handle }) { +function RoomShell({ ico, name, tag, status, desc, feats, children }) { + return ( +
+
+ +
+ {name} + {tag} +
+ {status === 'live' ? '● LIVE' : 'SOON'} +
+

{desc}

+ {feats &&
    {feats.map((f) =>
  • {f}
  • )}
} + {children} +
+ ); +} + +function SoonRoom(props) { + return ( + +
in development
+
+ ); +} + +function ShowcaseRoom({ loggedIn, handle, setRoute }) { const [list, setList] = useState([]); const [name, setName] = useState(''); const [created, setCreated] = useState(null); const [err, setErr] = useState(''); - const load = useCallback(async () => { try { const d = await api('/showcase'); setList(d.showcases || []); } catch (e) { setErr(e.message); } }, []); + const load = useCallback(async () => { + if (!loggedIn) return; + try { const d = await api('/showcase'); setList(d.showcases || []); } catch (e) { setErr(e.message); } + }, [loggedIn]); useEffect(() => { load(); }, [load]); const register = async (e) => { e.preventDefault(); setErr(''); @@ -34,38 +79,50 @@ function Showcase({ handle }) { catch (e2) { setErr(e2.message); } }; const del = async (id) => { if (!window.confirm('Delete this showcase?')) return; try { await api(`/showcase/${id}`, { method: 'DELETE' }); load(); } catch (e2) { setErr(e2.message); } }; - const copy = (t) => copyToast(t); return ( -
-
🌐
Showcasereverse proxy
-

Publish a web service from a local port to a public subdomain. Register a name, then in kebab's delivery center (δΊ€δ»˜δΈ­εΏƒ) map your local port β€” requests to the public URL tunnel through your broker.

-
- register - setName(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} required /> - .{handle}.amerc.ai - -
- {err &&

{err}

} - {created && ( -
-

βœ… {created.host} reserved Β· DNS {created.dns}

- {created.tls &&

πŸ”’ {created.tls}

} - - - -

In the kebab delivery center: connect a broker to the WS with this token, send {`{type:"bind",host:"${created.host}",port:}`}, and your local service is live at the public URL.

+ + {!loggedIn ? ( +
+
show1.{handle}.amerc.ai
+
+ ) : ( + <> +
+ register + setName(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} required /> + .{handle}.amerc.ai + +
+ {err &&

{err}

} + {created && ( +
+

βœ… {created.host} reserved Β· DNS {created.dns}

+ {created.tls &&

πŸ”’ {created.tls}

} + + + +

In the kebab delivery center: connect a broker to the WS with this token, send {`{type:"bind",host:"${created.host}",port:}`}, and your local service is live at the public URL.

+
+ )} +
+

My showcases

+ {list.map((s) => ( +
+ {s.host} + registered {new Date(s.created_at).toLocaleDateString()} + +
+ ))} + {!list.length &&

No showcases yet β€” register one above.

} +
+ )} -

My showcases

- {list.map((s) => ( -
- {s.host} - registered {new Date(s.created_at).toLocaleDateString()} - -
- ))} - {!list.length &&

No showcases yet β€” register one above.

} -
+ ); } diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 7dd9aba..28ffdfc 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -7,7 +7,7 @@ import Account from './Account.jsx'; import TavernGame from './TavernGame.jsx'; import { api } from './api.js'; -const RELEASE = '0.40.0-pwa'; +const RELEASE = '0.41.0-mansion'; const NAV = [ { id: 'home', label: 'Tavern' }, diff --git a/src/styles.css b/src/styles.css index 08ac781..325fd5f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1056,16 +1056,46 @@ button { } .hero-hot:hover { background: rgba(80,210,255,0.2) !important; box-shadow: 0 0 0 3px rgba(120,225,255,0.85), 0 0 26px rgba(80,210,255,0.6) !important; } -/* mansion (services hub) */ -.mn-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } -.mn-card { grid-column: 1 / -1; padding: 18px; border-radius: 14px; background: #130d20; border: 1px solid #2a1f3e; } -.mn-card-wide { max-width: 720px; } -.mn-card-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; } -.mn-card-head strong { font-size: 17px; color: #fff; display: block; } -.mn-soon-ico { font-size: 26px; } -.mn-tag { font-size: 11px; color: #27d8ff; } -.mn-soon { padding: 18px; border-radius: 14px; background: #0e0a18; border: 1px dashed #2a1f3e; opacity: 0.6; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; } -.mn-soon strong { color: #c9b8e6; } .mn-soon span { font-size: 12px; color: #7a6a98; } +/* mansion β€” services hall (0.41 vividness) */ +.mn-hero { display: flex; gap: 18px; align-items: center; padding: 22px 24px; margin-bottom: 22px; border-radius: 16px; + background: radial-gradient(120% 160% at 0% 0%, #2a1a3e 0%, #160d22 60%), #160d22; + border: 1px solid #34254e; box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 10px 30px rgba(0,0,0,0.35); position: relative; overflow: hidden; } +.mn-hero::after { content: ''; position: absolute; right: -40px; top: -40px; width: 200px; height: 200px; border-radius: 50%; + background: radial-gradient(circle, rgba(39,216,255,0.16), transparent 70%); pointer-events: none; } +.mn-hero-emblem { font-size: 46px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.5)); line-height: 1; } +.mn-hero-body h2 { margin: 0 0 6px; color: #fff; font-size: 24px; } +.mn-hero-body p { margin: 0 0 12px; color: #b6a7d4; font-size: 13.5px; line-height: 1.65; max-width: 70ch; } +.mn-hero-you { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: #8fe9c8; font-weight: 600; } +.mn-live-dot { width: 8px; height: 8px; border-radius: 50%; background: #36f0b0; box-shadow: 0 0 0 0 rgba(54,240,176,0.6); animation: mnpulse 2s infinite; } +@keyframes mnpulse { 0% { box-shadow: 0 0 0 0 rgba(54,240,176,0.5); } 70% { box-shadow: 0 0 0 7px rgba(54,240,176,0); } 100% { box-shadow: 0 0 0 0 rgba(54,240,176,0); } } + +.mn-rooms { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; align-items: start; } +.mn-room { padding: 18px; border-radius: 14px; background: linear-gradient(180deg, #160f24, #120c1d); border: 1px solid #2a1f3e; + transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s; } +.mn-room.live { grid-column: 1 / -1; border-color: #2f5e6e; box-shadow: 0 0 0 1px rgba(39,216,255,0.08), 0 8px 24px rgba(0,0,0,0.3); } +.mn-room.live:hover { border-color: #46c8e0; box-shadow: 0 0 0 1px rgba(39,216,255,0.18), 0 10px 30px rgba(0,0,0,0.4); } +.mn-room.soon { opacity: 0.92; } +.mn-room.soon:hover { transform: translateY(-2px); border-color: #3a2c54; } +.mn-room-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; } +.mn-room-ico { font-size: 28px; line-height: 1; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.4)); } +.mn-room-titles { display: flex; flex-direction: column; } +.mn-room-titles strong { font-size: 17px; color: #fff; } +.mn-room-tag { font-size: 11px; color: #9a8ac0; letter-spacing: 0.4px; } +.mn-badge { margin-left: auto; font-size: 10px; font-weight: 700; letter-spacing: 0.8px; padding: 4px 9px; border-radius: 999px; } +.mn-badge.live { color: #06222f; background: linear-gradient(180deg, #5fe6ff, #27d8ff); box-shadow: 0 0 12px rgba(39,216,255,0.4); } +.mn-badge.soon { color: #c9b8e6; background: #241a36; border: 1px solid #3a2c54; } +.mn-room-desc { color: #b0a2cd; font-size: 13px; line-height: 1.6; margin: 0 0 12px; } +.mn-feats { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; } +.mn-feats li { font-size: 11.5px; color: #cfe6ff; background: rgba(39,216,255,0.08); border: 1px solid rgba(39,216,255,0.18); padding: 3px 9px; border-radius: 999px; } +.mn-room.soon .mn-feats li { color: #c4b6e2; background: rgba(150,120,200,0.08); border-color: rgba(150,120,200,0.18); } +.mn-room-foot { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: #8576a6; text-transform: uppercase; letter-spacing: 0.6px; } +.mn-soon-bar { flex: 0 0 80px; height: 4px; border-radius: 3px; background: #241a36; overflow: hidden; position: relative; } +.mn-soon-bar i { position: absolute; left: -40%; width: 40%; height: 100%; border-radius: 3px; background: linear-gradient(90deg, transparent, #6f5aa0, transparent); animation: mnslide 1.8s infinite; } +@keyframes mnslide { 0% { left: -40%; } 100% { left: 100%; } } +.mn-room-cta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 4px; } +.mn-url-preview { font-family: ui-monospace, monospace; font-size: 13px; color: #6f7e98; background: #0a0f1a; border: 1px dashed #2e2440; padding: 8px 12px; border-radius: 8px; } +.mn-url-preview span { color: #27d8ff; } +.mn-mine { margin-top: 14px; } .mn-reg { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 12px 0; } .mn-reg input { padding: 9px 11px; border-radius: 8px; background: #0a0f1a; border: 1px solid #2e2440; color: #eaf2ff; font-family: inherit; width: 130px; } .mn-prefix, .mn-suffix { color: #8a7aa8; font-size: 13px; }