From 058ac24954e4f2aace79450eece1b494f7a74f97 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Fri, 28 Feb 2025 21:30:23 +0100 Subject: [PATCH 01/11] added first draft of the play route --- packages/www/package.json | 3 +- packages/www/src/App.tsx | 2 + packages/www/src/index.tsx | 10 ++- packages/www/src/pages/play.tsx | 149 ++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 packages/www/src/pages/play.tsx diff --git a/packages/www/package.json b/packages/www/package.json index 96c96968..fe20260c 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -27,6 +27,7 @@ "@solid-primitives/storage": "^4.3.1", "@solidjs/router": "^0.15.3", "modern-normalize": "^3.0.1", - "solid-js": "^1.9.5" + "solid-js": "^1.9.5", + "solid-notifications": "^1.1.2" } } \ No newline at end of file diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index efd3487d..59e032ad 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -7,6 +7,7 @@ import '@fontsource/geist-sans/700.css'; import '@fontsource/geist-sans/800.css'; import '@fontsource/geist-sans/900.css'; import { TeamCreate } from './pages/new'; +import { PlayComponent } from './pages/play'; import { styled } from "@macaron-css/solid"; import { useStorage } from './providers/account'; import { darkClass, lightClass, theme } from './ui/theme'; @@ -116,6 +117,7 @@ export const App: Component = () => { {WorkspaceRoute} */} + { diff --git a/packages/www/src/index.tsx b/packages/www/src/index.tsx index 608b9165..0bada0f9 100644 --- a/packages/www/src/index.tsx +++ b/packages/www/src/index.tsx @@ -8,6 +8,7 @@ import { render } from "solid-js/web"; import "modern-normalize/modern-normalize.css"; import { App } from "./App"; import { StorageProvider } from "./providers/account"; +import { ToastProvider, Toaster } from "solid-notifications"; const root = document.getElementById("root"); @@ -19,9 +20,12 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { render( () => ( - - - + + + + + + ), root! ); \ No newline at end of file diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx new file mode 100644 index 00000000..669f97fe --- /dev/null +++ b/packages/www/src/pages/play.tsx @@ -0,0 +1,149 @@ +import { Text } from "@nestri/www/ui/text"; +import { createSignal, createEffect, onCleanup, onMount } from "solid-js"; +import { useParams } from "@solidjs/router"; +import { Modal } from "@nestri/ui"; +import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; + +export function PlayComponent() { + const params = useParams(); + const id = params.id; + + const [showBannerModal, setShowBannerModal] = createSignal(false); + const [showButtonModal, setShowButtonModal] = createSignal(false); + const [gamepadConnected, setGamepadConnected] = createSignal(false); + const [buttonPressed, setButtonPressed] = createSignal(null); + const [leftStickX, setLeftStickX] = createSignal(0); + const [leftStickY, setLeftStickY] = createSignal(0); + const [hasStream, setHasStream] = createSignal(false); + const [showOffline, setShowOffline] = createSignal(false); + + const [canvas, setCanvas] = createSignal(undefined); + let video: HTMLVideoElement; + let webrtc: WebRTCStream; + let nestriMouse: Mouse , nestriKeyboard: Keyboard; + + const initializeInputDevices = () => { + const canvasElement = canvas(); + if (!canvasElement || !webrtc) return; + try { + nestriMouse = new Mouse({ canvas: canvasElement, webrtc }); + nestriKeyboard = new Keyboard({ canvas: canvasElement, webrtc }); + console.log("Input devices initialized successfully"); + } catch (error) { + console.error("Failed to initialize input devices:", error); + } + }; + + /*const initializeGamepad = () => { + console.log("Initializing gamepad..."); + + const updateGamepadState = () => { + const gamepads = navigator.getGamepads(); + const gamepad = gamepads[0]; + if (gamepad) { + setButtonPressed(gamepad.buttons.findIndex(btn => btn.pressed) !== -1 ? "Button pressed" : null); + setLeftStickX(Number(gamepad.axes[0].toFixed(2))); + setLeftStickY(Number(gamepad.axes[1].toFixed(2))); + } + requestAnimationFrame(updateGamepadState); + }; + + window.addEventListener("gamepadconnected", () => { + setGamepadConnected(true); + console.log("Gamepad connected!"); + updateGamepadState(); + }); + + window.addEventListener("gamepaddisconnected", () => { + setGamepadConnected(false); + console.log("Gamepad disconnected!"); + }); + };*/ + + const lockPlay = async () => { + const canvasElement = canvas(); + if (!canvasElement || !hasStream()) return; + try { + await canvasElement.requestPointerLock(); + await canvasElement.requestFullscreen(); + //initializeGamepad(); + } catch (error) { + console.error("Error during lock sequence:", error); + } + }; + + const setupPointerLockListener = () => { + document.addEventListener("pointerlockchange", () => { + const canvasElement = canvas(); + if (!canvasElement) return; + if (document.pointerLockElement === canvasElement) { + initializeInputDevices(); + } else { + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + } + }); + }; + + const handleVideoInput = async () => { + const canvasElement = canvas(); + if (!video || !canvasElement) return; + + try { + + await video.play(); + if (canvasElement && video) { + canvasElement.width = video.videoWidth; + canvasElement.height = video.videoHeight; + + + const ctx = canvasElement.getContext("2d"); + const renderer = () => { + if (ctx && hasStream() && video) { + ctx.drawImage(video, 0, 0); + video.requestVideoFrameCallback(renderer); + } + }; + + video.requestVideoFrameCallback(renderer); + } + } catch (error) { + console.error("Error playing video:", error); + } + }; + + + onMount(() => { + setupPointerLockListener(); + video = document.createElement("video"); + video.style.visibility = "hidden"; + webrtc = new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => { + if (video && mediaStream) { + video.srcObject = mediaStream; + setHasStream(true); + setShowOffline(false); + await handleVideoInput(); + } else { + setShowOffline(true); + setHasStream(false); + } + }); + }); + + onCleanup(() => { + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + }); + + return ( + <> + {showOffline() ? ( + + Offline + + ) : ( + + )} + > + ); + } \ No newline at end of file From 90e0533fdd384c5ba4392cc5f72de99a915a1eb6 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Fri, 28 Feb 2025 21:47:46 +0100 Subject: [PATCH 02/11] right parameter --- packages/www/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 59e032ad..965e7da9 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -117,7 +117,7 @@ export const App: Component = () => { {WorkspaceRoute} */} - + { From 805a8a611571c3bb7dfea9836f6d9b4bfd29d654 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 17:39:38 +0100 Subject: [PATCH 03/11] some changes to the play route --- packages/www/src/pages/play.tsx | 106 ++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 669f97fe..8c9dbb25 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,8 +1,8 @@ import { Text } from "@nestri/www/ui/text"; -import { createSignal, createEffect, onCleanup, onMount } from "solid-js"; +import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"; import { useParams } from "@solidjs/router"; -import { Modal } from "@nestri/ui"; import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; +import { Container, FullScreen } from "@nestri/www/ui/layout"; export function PlayComponent() { const params = useParams(); @@ -15,6 +15,7 @@ export function PlayComponent() { const [leftStickX, setLeftStickX] = createSignal(0); const [leftStickY, setLeftStickY] = createSignal(0); const [hasStream, setHasStream] = createSignal(false); + const [nestriLock, setNestriLock] = createSignal(false); const [showOffline, setShowOffline] = createSignal(false); const [canvas, setCanvas] = createSignal(undefined); @@ -67,6 +68,25 @@ export function PlayComponent() { await canvasElement.requestPointerLock(); await canvasElement.requestFullscreen(); //initializeGamepad(); + + if (document.fullscreenElement !== null) { + if ('keyboard' in navigator && 'lock' in (navigator.keyboard as any)) { + const keys = [ + "AltLeft", "AltRight", "Tab", "Escape", + "ContextMenu", "MetaLeft", "MetaRight" + ]; + + try { + await (navigator.keyboard as any).lock(keys); + setNestriLock(true); + console.log("Keyboard lock acquired"); + } catch (e) { + console.warn("Keyboard lock failed:", e); + setNestriLock(false); + } + } + } + } catch (error) { console.error("Error during lock sequence:", error); } @@ -79,6 +99,15 @@ export function PlayComponent() { if (document.pointerLockElement === canvasElement) { initializeInputDevices(); } else { + + if (!showBannerModal) { + const playing = sessionStorage.getItem("showedBanner"); + setShowBannerModal(!playing || playing !== "true"); + setShowButtonModal(playing === "false"); + } + + + nestriKeyboard?.dispose(); nestriMouse?.dispose(); } @@ -114,6 +143,9 @@ export function PlayComponent() { onMount(() => { + const canvasElement = canvas(); + if(!canvasElement) return; + setupPointerLockListener(); video = document.createElement("video"); video.style.visibility = "hidden"; @@ -123,9 +155,17 @@ export function PlayComponent() { setHasStream(true); setShowOffline(false); await handleVideoInput(); - } else { + } else if (mediaStream === null) { + console.log("MediaStream is null, Room is offline"); setShowOffline(true); setHasStream(false); + + const ctx = canvasElement.getContext("2d"); + if (ctx) ctx.clearRect(0, 0, canvasElement.width, canvasElement.height); + } else if (video && video.srcObject !== null) { + setHasStream(true); + setShowOffline(true); + await handleVideoInput(); } }); }); @@ -136,14 +176,70 @@ export function PlayComponent() { }); return ( - <> + {showOffline() ? ( Offline + setShowButtonModal(true)}>Show Modal ) : ( )} - > + + + + ); + } + + interface ModalProps { + show: () => boolean; + setShow: (value: boolean) => void; + closeOnBackdropClick?: boolean; + handleVideoInput?: () => Promise; + lockPlay?: () => Promise; + } + + function Modal(props: ModalProps) { + return ( + + props.closeOnBackdropClick && props.setShow(false)} + > + e.stopPropagation()} // Prevent closing when clicking inside modal + > + + + { + props.setShow(false); + sessionStorage.setItem("showedBanner", "true"); + await props.handleVideoInput?.(); + await props.lockPlay?.(); + }} + > + Continue Playing + + + Shutdown Nestri + + + + + + ); } \ No newline at end of file From a727a9b710a629c952fed7a67b6719b8c809b2fc Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 21:07:13 +0100 Subject: [PATCH 04/11] some wip with styles --- packages/www/src/pages/play.tsx | 95 ++++++++++++++++++++++++++------- packages/www/src/ui/theme.ts | 9 +++- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 8c9dbb25..5bc4712e 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,9 +1,36 @@ import { Text } from "@nestri/www/ui/text"; -import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"; +import { createSignal, createEffect, onCleanup, onMount, Show} from "solid-js"; +import { Portal } from "solid-js/web"; import { useParams } from "@solidjs/router"; import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; import { Container, FullScreen } from "@nestri/www/ui/layout"; +import { styled } from "@macaron-css/solid"; +import { theme } from "../ui/theme"; +const Canvas = styled("canvas", { + base: { + aspectRatio: 16 / 9, + width: "100%", + height: "100%", + objectFit: "contain", + maxHeight: "100vh", + }}); + + const ModalContainer = styled("div", { + base: { + width: "100%", + maxWidth: 370, + maxHeight: "75vh", + borderRadius: 12, + borderWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d400, + backgroundColor: theme.color.pink.d400, + boxShadow: theme.color.boxShadow, + backdropFilter: "blur(20px)", + padding: "20px 25px" + } + }) export function PlayComponent() { const params = useParams(); const id = params.id; @@ -149,7 +176,7 @@ export function PlayComponent() { setupPointerLockListener(); video = document.createElement("video"); video.style.visibility = "hidden"; - webrtc = new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => { + webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { if (video && mediaStream) { video.srcObject = mediaStream; setHasStream(true); @@ -175,15 +202,23 @@ export function PlayComponent() { nestriMouse?.dispose(); }); + const { Modal, openModal } = createModal(); + return ( - + <> + + open modal + + + > + /* {showOffline() ? ( Offline setShowButtonModal(true)}>Show Modal ) : ( - + )} - + */ ); } @@ -203,19 +238,43 @@ export function PlayComponent() { lockPlay?: () => Promise; } + function createModal() { + const [open, setOpen] = createSignal(false); + + return { + openModal() { + setOpen(true); + }, + Modal() { + return ( + + + + + Hello from modal + setOpen(false)}>close modal + + + + + ); + }, + }; + } + function Modal(props: ModalProps) { return ( - - props.closeOnBackdropClick && props.setShow(false)} - > - e.stopPropagation()} // Prevent closing when clicking inside modal > @@ -238,8 +297,6 @@ export function PlayComponent() { - - - + ); } \ No newline at end of file diff --git a/packages/www/src/ui/theme.ts b/packages/www/src/ui/theme.ts index 31c223b1..c51974ef 100644 --- a/packages/www/src/ui/theme.ts +++ b/packages/www/src/ui/theme.ts @@ -233,6 +233,7 @@ const light = (() => { }, }; + const boxShadow = "0 0 0 1px rgba(19,21,23,0.08), 0 3.3px 2.7px rgba(0,0,0,.03),0 8.3px 6.9px rgba(0,0,0,.04),0 17px 14.2px rgba(0,0,0,.05),0 35px 29.2px rgba(0,0,0,.06),0px -4px 4px 0px rgba(0,0,0,.07) inset"; return { gray, blue, @@ -248,7 +249,8 @@ const light = (() => { focusBorder, focusColor, d1000, - text + text, + boxShadow }; })() @@ -391,6 +393,8 @@ const dark = (() => { }, }; + const boxShadow = "0 0 0 1px rgba(255,255,255,0.08), 0 3.3px 2.7px rgba(0,0,0,.1),0 8.3px 6.9px rgba(0,0,0,.13),0 17px 14.2px rgba(0,0,0,.17),0 35px 29.2px rgba(0,0,0,.22),0px -4px 4px 0px rgba(0,0,0,.04) inset"; + return { gray, blue, @@ -406,7 +410,8 @@ const dark = (() => { focusBorder, focusColor, d1000, - text + text, + boxShadow }; })() From c994dc112c86abc3e661bcecbe0e3cf13a8be717 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 21:16:27 +0100 Subject: [PATCH 05/11] added new App changes and background for theme --- packages/www/src/App.tsx | 29 ++--------------------------- packages/www/src/ui/theme.ts | 9 +++++---- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 965e7da9..aad73ca6 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -6,7 +6,6 @@ import '@fontsource/geist-sans/600.css'; import '@fontsource/geist-sans/700.css'; import '@fontsource/geist-sans/800.css'; import '@fontsource/geist-sans/900.css'; -import { TeamCreate } from './pages/new'; import { PlayComponent } from './pages/play'; import { styled } from "@macaron-css/solid"; import { useStorage } from './providers/account'; @@ -35,10 +34,10 @@ globalStyle("html", { // Hardcode colors "@media": { "(prefers-color-scheme: light)": { - backgroundColor: "hsla(0,0%,98%)", + backgroundColor: "#f5f5f5", }, "(prefers-color-scheme: dark)": { - backgroundColor: "hsla(0,0%,0%)", + backgroundColor: "#1e1e1e", }, }, }); @@ -91,32 +90,8 @@ export const App: Component = () => { {props.children} - // - // - // - // - // - // - // - // - // - // {props.children} - // - // - // - // - // - // - // - // )} > - {/* - - - - {WorkspaceRoute} */} - { } const background = { - d100: 'hsla(0,0%,100%)', - d200: 'hsla(0,0%,98%)' + d100: '#f5f5f5', + d200: 'oklch(from #f5f5f5 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)' }; const contrastFg = '#ffffff'; @@ -371,9 +371,10 @@ const dark = (() => { } const background = { - d100: 'hsla(0,0%,4%)', - d200: 'hsla(0,0%,0%)' + d200: '#171717', + d100: "oklch(from #171717 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)" }; + const contrastFg = '#ffffff'; const focusBorder = `0 0 0 1px ${grayAlpha.d600}, 0px 0px 0px 4px rgba(255,255,255,0.24)`; const focusColor = blue.d900 From 1e782385934ec684a1a84a2c1bc1eec72eab16c1 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 3 Mar 2025 23:36:18 +0300 Subject: [PATCH 06/11] fix: Colors --- packages/www/src/App.tsx | 9 +- packages/www/src/index.tsx | 8 +- packages/www/src/pages/play.tsx | 542 +++++++++++++++++--------------- packages/www/src/pages/test.tsx | 18 ++ 4 files changed, 309 insertions(+), 268 deletions(-) create mode 100644 packages/www/src/pages/test.tsx diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index aad73ca6..766feb33 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -14,6 +14,7 @@ import { AuthProvider, useAuth } from './providers/auth'; import { Navigate, Route, Router } from "@solidjs/router"; import { globalStyle, macaron$ } from "@macaron-css/core"; import { Component, createSignal, Match, onCleanup, Switch } from 'solid-js'; +import TestComponent from './pages/test'; const Root = styled("div", { base: { @@ -87,12 +88,14 @@ export const App: Component = () => { ( - - {props.children} - + // + // {props.children} + props.children + // )} > + { diff --git a/packages/www/src/index.tsx b/packages/www/src/index.tsx index 0bada0f9..80a66cb8 100644 --- a/packages/www/src/index.tsx +++ b/packages/www/src/index.tsx @@ -8,7 +8,7 @@ import { render } from "solid-js/web"; import "modern-normalize/modern-normalize.css"; import { App } from "./App"; import { StorageProvider } from "./providers/account"; -import { ToastProvider, Toaster } from "solid-notifications"; +// import { ToastProvider, Toaster } from "solid-notifications"; const root = document.getElementById("root"); @@ -20,12 +20,12 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { render( () => ( - - + // + // - + // ), root! ); \ No newline at end of file diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 5bc4712e..66f7bc67 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,11 +1,11 @@ import { Text } from "@nestri/www/ui/text"; -import { createSignal, createEffect, onCleanup, onMount, Show} from "solid-js"; +import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"; import { Portal } from "solid-js/web"; import { useParams } from "@solidjs/router"; import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; import { Container, FullScreen } from "@nestri/www/ui/layout"; import { styled } from "@macaron-css/solid"; -import { theme } from "../ui/theme"; +import { lightClass, theme, darkClass } from "@nestri/www/ui/theme"; const Canvas = styled("canvas", { base: { @@ -14,289 +14,309 @@ const Canvas = styled("canvas", { height: "100%", objectFit: "contain", maxHeight: "100vh", - }}); - - const ModalContainer = styled("div", { - base: { - width: "100%", - maxWidth: 370, - maxHeight: "75vh", - borderRadius: 12, - borderWidth: 1, - borderStyle: "solid", - borderColor: theme.color.gray.d400, - backgroundColor: theme.color.pink.d400, - boxShadow: theme.color.boxShadow, - backdropFilter: "blur(20px)", - padding: "20px 25px" - } - }) + } +}); + +const ModalContainer = styled("div", { + base: { + width: "100%", + maxWidth: 370, + maxHeight: "75vh", + height: "auto", + // borderRadius: 12, + // borderWidth: 1, + // borderStyle: "solid", + // borderColor: theme.color.gray.d400, + // backgroundColor: theme.color.pink.d400, + backgroundColor: theme.color.red.d300, + // boxShadow: theme.color.boxShadow, + // backdropFilter: "blur(20px)", + padding: "20px 25px" + } +}) + export function PlayComponent() { - const params = useParams(); - const id = params.id; - - const [showBannerModal, setShowBannerModal] = createSignal(false); - const [showButtonModal, setShowButtonModal] = createSignal(false); - const [gamepadConnected, setGamepadConnected] = createSignal(false); - const [buttonPressed, setButtonPressed] = createSignal(null); - const [leftStickX, setLeftStickX] = createSignal(0); - const [leftStickY, setLeftStickY] = createSignal(0); - const [hasStream, setHasStream] = createSignal(false); - const [nestriLock, setNestriLock] = createSignal(false); - const [showOffline, setShowOffline] = createSignal(false); + const params = useParams(); + const id = params.id; + + const [showBannerModal, setShowBannerModal] = createSignal(false); + const [showButtonModal, setShowButtonModal] = createSignal(false); + const [gamepadConnected, setGamepadConnected] = createSignal(false); + const [buttonPressed, setButtonPressed] = createSignal(null); + const [leftStickX, setLeftStickX] = createSignal(0); + const [leftStickY, setLeftStickY] = createSignal(0); + const [hasStream, setHasStream] = createSignal(false); + const [nestriLock, setNestriLock] = createSignal(false); + const [showOffline, setShowOffline] = createSignal(false); + + const [canvas, setCanvas] = createSignal(undefined); + let video: HTMLVideoElement; + let webrtc: WebRTCStream; + let nestriMouse: Mouse, nestriKeyboard: Keyboard; + + const initializeInputDevices = () => { + const canvasElement = canvas(); + if (!canvasElement || !webrtc) return; + try { + nestriMouse = new Mouse({ canvas: canvasElement, webrtc }); + nestriKeyboard = new Keyboard({ canvas: canvasElement, webrtc }); + console.log("Input devices initialized successfully"); + } catch (error) { + console.error("Failed to initialize input devices:", error); + } + }; + + /*const initializeGamepad = () => { + console.log("Initializing gamepad..."); - const [canvas, setCanvas] = createSignal(undefined); - let video: HTMLVideoElement; - let webrtc: WebRTCStream; - let nestriMouse: Mouse , nestriKeyboard: Keyboard; - - const initializeInputDevices = () => { - const canvasElement = canvas(); - if (!canvasElement || !webrtc) return; - try { - nestriMouse = new Mouse({ canvas: canvasElement, webrtc }); - nestriKeyboard = new Keyboard({ canvas: canvasElement, webrtc }); - console.log("Input devices initialized successfully"); - } catch (error) { - console.error("Failed to initialize input devices:", error); - } + const updateGamepadState = () => { + const gamepads = navigator.getGamepads(); + const gamepad = gamepads[0]; + if (gamepad) { + setButtonPressed(gamepad.buttons.findIndex(btn => btn.pressed) !== -1 ? "Button pressed" : null); + setLeftStickX(Number(gamepad.axes[0].toFixed(2))); + setLeftStickY(Number(gamepad.axes[1].toFixed(2))); + } + requestAnimationFrame(updateGamepadState); }; - - /*const initializeGamepad = () => { - console.log("Initializing gamepad..."); - - const updateGamepadState = () => { - const gamepads = navigator.getGamepads(); - const gamepad = gamepads[0]; - if (gamepad) { - setButtonPressed(gamepad.buttons.findIndex(btn => btn.pressed) !== -1 ? "Button pressed" : null); - setLeftStickX(Number(gamepad.axes[0].toFixed(2))); - setLeftStickY(Number(gamepad.axes[1].toFixed(2))); - } - requestAnimationFrame(updateGamepadState); - }; - - window.addEventListener("gamepadconnected", () => { - setGamepadConnected(true); - console.log("Gamepad connected!"); - updateGamepadState(); - }); - - window.addEventListener("gamepaddisconnected", () => { - setGamepadConnected(false); - console.log("Gamepad disconnected!"); - }); - };*/ - - const lockPlay = async () => { - const canvasElement = canvas(); - if (!canvasElement || !hasStream()) return; - try { - await canvasElement.requestPointerLock(); - await canvasElement.requestFullscreen(); - //initializeGamepad(); - - if (document.fullscreenElement !== null) { - if ('keyboard' in navigator && 'lock' in (navigator.keyboard as any)) { - const keys = [ - "AltLeft", "AltRight", "Tab", "Escape", - "ContextMenu", "MetaLeft", "MetaRight" - ]; - - try { - await (navigator.keyboard as any).lock(keys); - setNestriLock(true); - console.log("Keyboard lock acquired"); - } catch (e) { - console.warn("Keyboard lock failed:", e); - setNestriLock(false); - } - } - } - - } catch (error) { - console.error("Error during lock sequence:", error); - } - }; - - const setupPointerLockListener = () => { - document.addEventListener("pointerlockchange", () => { - const canvasElement = canvas(); - if (!canvasElement) return; - if (document.pointerLockElement === canvasElement) { - initializeInputDevices(); - } else { - - if (!showBannerModal) { - const playing = sessionStorage.getItem("showedBanner"); - setShowBannerModal(!playing || playing !== "true"); - setShowButtonModal(playing === "false"); - } + + window.addEventListener("gamepadconnected", () => { + setGamepadConnected(true); + console.log("Gamepad connected!"); + updateGamepadState(); + }); + + window.addEventListener("gamepaddisconnected", () => { + setGamepadConnected(false); + console.log("Gamepad disconnected!"); + }); + };*/ - + const lockPlay = async () => { + const canvasElement = canvas(); + if (!canvasElement || !hasStream()) return; + try { + await canvasElement.requestPointerLock(); + await canvasElement.requestFullscreen(); + //initializeGamepad(); - nestriKeyboard?.dispose(); - nestriMouse?.dispose(); - } - }); - }; + if (document.fullscreenElement !== null) { + if ('keyboard' in navigator && 'lock' in (navigator.keyboard as any)) { + const keys = [ + "AltLeft", "AltRight", "Tab", "Escape", + "ContextMenu", "MetaLeft", "MetaRight" + ]; - const handleVideoInput = async () => { - const canvasElement = canvas(); - if (!video || !canvasElement) return; - - try { - - await video.play(); - if (canvasElement && video) { - canvasElement.width = video.videoWidth; - canvasElement.height = video.videoHeight; - - - const ctx = canvasElement.getContext("2d"); - const renderer = () => { - if (ctx && hasStream() && video) { - ctx.drawImage(video, 0, 0); - video.requestVideoFrameCallback(renderer); - } - }; - - video.requestVideoFrameCallback(renderer); - } - } catch (error) { - console.error("Error playing video:", error); + try { + await (navigator.keyboard as any).lock(keys); + setNestriLock(true); + console.log("Keyboard lock acquired"); + } catch (e) { + console.warn("Keyboard lock failed:", e); + setNestriLock(false); + } } - }; - - - onMount(() => { + } + + } catch (error) { + console.error("Error during lock sequence:", error); + } + }; + + const setupPointerLockListener = () => { + document.addEventListener("pointerlockchange", () => { const canvasElement = canvas(); - if(!canvasElement) return; - - setupPointerLockListener(); - video = document.createElement("video"); - video.style.visibility = "hidden"; - webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { - if (video && mediaStream) { - video.srcObject = mediaStream; - setHasStream(true); - setShowOffline(false); - await handleVideoInput(); - } else if (mediaStream === null) { - console.log("MediaStream is null, Room is offline"); - setShowOffline(true); - setHasStream(false); - - const ctx = canvasElement.getContext("2d"); - if (ctx) ctx.clearRect(0, 0, canvasElement.width, canvasElement.height); - } else if (video && video.srcObject !== null) { - setHasStream(true); - setShowOffline(true); - await handleVideoInput(); + if (!canvasElement) return; + if (document.pointerLockElement === canvasElement) { + initializeInputDevices(); + } else { + + if (!showBannerModal) { + const playing = sessionStorage.getItem("showedBanner"); + setShowBannerModal(!playing || playing !== "true"); + setShowButtonModal(playing === "false"); } - }); + + + + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + } }); - - onCleanup(() => { - nestriKeyboard?.dispose(); - nestriMouse?.dispose(); + }; + + const handleVideoInput = async () => { + const canvasElement = canvas(); + if (!video || !canvasElement) return; + + try { + + await video.play(); + if (canvasElement && video) { + canvasElement.width = video.videoWidth; + canvasElement.height = video.videoHeight; + + + const ctx = canvasElement.getContext("2d"); + const renderer = () => { + if (ctx && hasStream() && video) { + ctx.drawImage(video, 0, 0); + video.requestVideoFrameCallback(renderer); + } + }; + + video.requestVideoFrameCallback(renderer); + } + } catch (error) { + console.error("Error playing video:", error); + } + }; + + + onMount(() => { + const canvasElement = canvas(); + if (!canvasElement) return; + + setupPointerLockListener(); + video = document.createElement("video"); + video.style.visibility = "hidden"; + webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { + if (video && mediaStream) { + video.srcObject = mediaStream; + setHasStream(true); + setShowOffline(false); + await handleVideoInput(); + } else if (mediaStream === null) { + console.log("MediaStream is null, Room is offline"); + setShowOffline(true); + setHasStream(false); + + const ctx = canvasElement.getContext("2d"); + if (ctx) ctx.clearRect(0, 0, canvasElement.width, canvasElement.height); + } else if (video && video.srcObject !== null) { + setHasStream(true); + setShowOffline(true); + await handleVideoInput(); + } }); - - const { Modal, openModal } = createModal(); + }); - return ( - <> + onCleanup(() => { + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + }); + + const { Modal, openModal } = createModal(); + + return ( + <> open modal > - /* - {showOffline() ? ( - - Offline - setShowButtonModal(true)}>Show Modal - - ) : ( - - )} - - - */ - ); - } + /* + {showOffline() ? ( + + Offline + setShowButtonModal(true)}>Show Modal + + ) : ( + + )} + + + */ + ); +} + +interface ModalProps { + show: () => boolean; + setShow: (value: boolean) => void; + closeOnBackdropClick?: boolean; + handleVideoInput?: () => Promise; + lockPlay?: () => Promise; +} + +function createModal() { + const [open, setOpen] = createSignal(false); + const [theme, setTheme] = createSignal( + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + ); + + const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); + const setColorScheme = (e: MediaQueryListEvent) => { + setTheme(e.matches ? "dark" : "light"); + }; + darkMode.addEventListener("change", setColorScheme); + onCleanup(() => { + darkMode.removeEventListener("change", setColorScheme); + }); - interface ModalProps { - show: () => boolean; - setShow: (value: boolean) => void; - closeOnBackdropClick?: boolean; - handleVideoInput?: () => Promise; - lockPlay?: () => Promise; - } - function createModal() { - const [open, setOpen] = createSignal(false); - - return { - openModal() { - setOpen(true); - }, - Modal() { - return ( - - - + + - - Hello from modal - setOpen(false)}>close modal - - - - - ); - }, - }; - } + > + + Hello from modal + setOpen(false)}>close modal + + + + + ); + }, + }; +} + +function Modal(props: ModalProps) { + return ( - function Modal(props: ModalProps) { - return ( - - - e.stopPropagation()} // Prevent closing when clicking inside modal + + e.stopPropagation()} // Prevent closing when clicking inside modal + > + + + { + props.setShow(false); + sessionStorage.setItem("showedBanner", "true"); + await props.handleVideoInput?.(); + await props.lockPlay?.(); + }} > - - - { - props.setShow(false); - sessionStorage.setItem("showedBanner", "true"); - await props.handleVideoInput?.(); - await props.lockPlay?.(); - }} - > - Continue Playing - - - Shutdown Nestri - - - - - ); - } \ No newline at end of file + Continue Playing + + + Shutdown Nestri + + + + + ); +} \ No newline at end of file diff --git a/packages/www/src/pages/test.tsx b/packages/www/src/pages/test.tsx new file mode 100644 index 00000000..f6c7c4bb --- /dev/null +++ b/packages/www/src/pages/test.tsx @@ -0,0 +1,18 @@ +import { styled } from "@macaron-css/solid"; +import { theme } from "../ui/theme"; + + +const Testing = styled("div", { + base: { + height: "100%", + width: "100%", + position: "fixed", + backgroundColor: theme.color.blue.d600 + } +}) + +export default function TestComponent() { + return ( + + ) +} \ No newline at end of file From 4fd339b55feeb747ec7c9e03f66515fe37f04075 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 3 Mar 2025 23:42:58 +0300 Subject: [PATCH 07/11] fix: Have a root component --- packages/www/src/App.tsx | 43 ++++++++++++--------------------- packages/www/src/pages/play.tsx | 35 +++++++++------------------ packages/www/src/ui/root.tsx | 40 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 packages/www/src/ui/root.tsx diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 766feb33..1b960f0f 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -15,19 +15,20 @@ import { Navigate, Route, Router } from "@solidjs/router"; import { globalStyle, macaron$ } from "@macaron-css/core"; import { Component, createSignal, Match, onCleanup, Switch } from 'solid-js'; import TestComponent from './pages/test'; +import Root from './ui/root'; -const Root = styled("div", { - base: { - inset: 0, - lineHeight: 1, - fontSynthesis: "none", - color: theme.color.d1000.gray, - fontFamily: theme.font.family.body, - textRendering: "optimizeLegibility", - WebkitFontSmoothing: "antialised", - backgroundColor: theme.color.background.d100, - }, -}); +// const Root = styled("div", { +// base: { +// inset: 0, +// lineHeight: 1, +// fontSynthesis: "none", +// color: theme.color.d1000.gray, +// fontFamily: theme.font.family.body, +// textRendering: "optimizeLegibility", +// WebkitFontSmoothing: "antialised", +// backgroundColor: theme.color.background.d100, +// }, +// }); globalStyle("html", { fontSize: 16, @@ -65,25 +66,11 @@ globalStyle("*", { }); export const App: Component = () => { - const [theme, setTheme] = createSignal( - window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light", - ); - - const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); - const setColorScheme = (e: MediaQueryListEvent) => { - setTheme(e.matches ? "dark" : "light"); - }; - darkMode.addEventListener("change", setColorScheme); - onCleanup(() => { - darkMode.removeEventListener("change", setColorScheme); - }); - + const storage = useStorage(); return ( - + ( - window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light", - ); - - const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); - const setColorScheme = (e: MediaQueryListEvent) => { - setTheme(e.matches ? "dark" : "light"); - }; - darkMode.addEventListener("change", setColorScheme); - onCleanup(() => { - darkMode.removeEventListener("change", setColorScheme); - }); - return { openModal() { @@ -268,21 +254,22 @@ function createModal() { return ( - + - - Hello from modal - setOpen(false)}>close modal - - + > + + Hello from modal + setOpen(false)}>close modal + + + ); diff --git a/packages/www/src/ui/root.tsx b/packages/www/src/ui/root.tsx new file mode 100644 index 00000000..7e06d7a8 --- /dev/null +++ b/packages/www/src/ui/root.tsx @@ -0,0 +1,40 @@ +import { createSignal, JSX, onCleanup } from "solid-js"; +import { useStorage } from "@nestri/www/providers/account"; +import { darkClass, lightClass, theme } from "./theme"; +import { styled } from "@macaron-css/solid"; + +const BaseComponent = styled("div", { + base: { + inset: 0, + lineHeight: 1, + fontSynthesis: "none", + color: theme.color.d1000.gray, + fontFamily: theme.font.family.body, + textRendering: "optimizeLegibility", + WebkitFontSmoothing: "antialised", + backgroundColor: theme.color.background.d100, + }, +}); + +export default function Root(props: { children: number | boolean | Node | JSX.ArrayElement | (string & {}) | null | undefined; }) { + const [theme, setTheme] = createSignal( + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + ); + + const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); + const setColorScheme = (e: MediaQueryListEvent) => { + setTheme(e.matches ? "dark" : "light"); + }; + darkMode.addEventListener("change", setColorScheme); + onCleanup(() => { + darkMode.removeEventListener("change", setColorScheme); + }); + + return ( + + {props.children} + + ) +} \ No newline at end of file From fb0cb0b6cae59c81a0f4c60b49606df53fb9607e Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 3 Mar 2025 23:47:00 +0300 Subject: [PATCH 08/11] fix: Portal mount --- packages/www/src/App.tsx | 43 +++++++++++++++++++++------------ packages/www/src/pages/play.tsx | 5 +--- packages/www/src/ui/root.tsx | 40 ------------------------------ 3 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 packages/www/src/ui/root.tsx diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 1b960f0f..766feb33 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -15,20 +15,19 @@ import { Navigate, Route, Router } from "@solidjs/router"; import { globalStyle, macaron$ } from "@macaron-css/core"; import { Component, createSignal, Match, onCleanup, Switch } from 'solid-js'; import TestComponent from './pages/test'; -import Root from './ui/root'; -// const Root = styled("div", { -// base: { -// inset: 0, -// lineHeight: 1, -// fontSynthesis: "none", -// color: theme.color.d1000.gray, -// fontFamily: theme.font.family.body, -// textRendering: "optimizeLegibility", -// WebkitFontSmoothing: "antialised", -// backgroundColor: theme.color.background.d100, -// }, -// }); +const Root = styled("div", { + base: { + inset: 0, + lineHeight: 1, + fontSynthesis: "none", + color: theme.color.d1000.gray, + fontFamily: theme.font.family.body, + textRendering: "optimizeLegibility", + WebkitFontSmoothing: "antialised", + backgroundColor: theme.color.background.d100, + }, +}); globalStyle("html", { fontSize: 16, @@ -66,11 +65,25 @@ globalStyle("*", { }); export const App: Component = () => { - + const [theme, setTheme] = createSignal( + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + ); + + const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); + const setColorScheme = (e: MediaQueryListEvent) => { + setTheme(e.matches ? "dark" : "light"); + }; + darkMode.addEventListener("change", setColorScheme); + onCleanup(() => { + darkMode.removeEventListener("change", setColorScheme); + }); + const storage = useStorage(); return ( - + + - setOpen(false)}>close modal - ); diff --git a/packages/www/src/ui/root.tsx b/packages/www/src/ui/root.tsx deleted file mode 100644 index 7e06d7a8..00000000 --- a/packages/www/src/ui/root.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createSignal, JSX, onCleanup } from "solid-js"; -import { useStorage } from "@nestri/www/providers/account"; -import { darkClass, lightClass, theme } from "./theme"; -import { styled } from "@macaron-css/solid"; - -const BaseComponent = styled("div", { - base: { - inset: 0, - lineHeight: 1, - fontSynthesis: "none", - color: theme.color.d1000.gray, - fontFamily: theme.font.family.body, - textRendering: "optimizeLegibility", - WebkitFontSmoothing: "antialised", - backgroundColor: theme.color.background.d100, - }, -}); - -export default function Root(props: { children: number | boolean | Node | JSX.ArrayElement | (string & {}) | null | undefined; }) { - const [theme, setTheme] = createSignal( - window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light", - ); - - const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); - const setColorScheme = (e: MediaQueryListEvent) => { - setTheme(e.matches ? "dark" : "light"); - }; - darkMode.addEventListener("change", setColorScheme); - onCleanup(() => { - darkMode.removeEventListener("change", setColorScheme); - }); - - return ( - - {props.children} - - ) -} \ No newline at end of file From 402e89422443a2825f0209da0c4c0f67b92b78c3 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 22:28:53 +0100 Subject: [PATCH 09/11] added modal with correct styling. Open: need to wire the modals correctly and have the welcome modal --- packages/www/src/pages/play.tsx | 126 +++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 35 deletions(-) diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 067dbbcf..6df0e5ca 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,3 +1,5 @@ +// FIXME: We need to make from the modal a reusable component +// FIXME: The mousepointer lock is somehow shifted when the window gets resized import { Text } from "@nestri/www/ui/text"; import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"; import { Portal } from "solid-js/web"; @@ -23,18 +25,33 @@ const ModalContainer = styled("div", { maxWidth: 370, maxHeight: "75vh", height: "auto", - // borderRadius: 12, - // borderWidth: 1, - // borderStyle: "solid", - // borderColor: theme.color.gray.d400, - // backgroundColor: theme.color.pink.d400, - backgroundColor: theme.color.red.d300, - // boxShadow: theme.color.boxShadow, - // backdropFilter: "blur(20px)", + borderRadius: 12, + borderWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d400, + backgroundColor: theme.color.gray.d200, + boxShadow: theme.color.boxShadow, + backdropFilter: "blur(20px)", padding: "20px 25px" } }) +const Button = styled("button", { + base: { + outline: "none", + width: "100%", + backgroundColor: theme.color.background.d100, + padding: "12px 16px", + borderRadius: 10, + borderWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d500, + ":hover": { + backgroundColor: theme.color.gray.d300, + } + } +}) + export function PlayComponent() { const params = useParams(); const id = params.id; @@ -54,6 +71,8 @@ export function PlayComponent() { let webrtc: WebRTCStream; let nestriMouse: Mouse, nestriKeyboard: Keyboard; + const { Modal, openModal } = createModal(); + const initializeInputDevices = () => { const canvasElement = canvas(); if (!canvasElement || !webrtc) return; @@ -134,7 +153,10 @@ export function PlayComponent() { if (!showBannerModal) { const playing = sessionStorage.getItem("showedBanner"); setShowBannerModal(!playing || playing !== "true"); - setShowButtonModal(playing === "false"); + if(!playing) { + openModal(); + } + } @@ -206,31 +228,21 @@ export function PlayComponent() { nestriMouse?.dispose(); }); - const { Modal, openModal } = createModal(); - - return ( - <> - - open modal - - - > - /* + return ( {showOffline() ? ( Offline - setShowButtonModal(true)}>Show Modal ) : ( )} - - */ + + ); } @@ -242,6 +254,44 @@ interface ModalProps { lockPlay?: () => Promise; } +function createWelcomeModal() { + const [open, setOpen] = createSignal(false); + + return { + openWelcomeModal() { + setOpen(true); + }, + WelcomeModal(props: ModalProps) { + return ( + + + + + + Happy that you use Nestri! + { + sessionStorage.setItem("showedBanner", "true"); + await props.handleVideoInput?.(); + await props.lockPlay?.(); + }}>Let's go + + + + + + ); + }, + }; +} + function createModal() { const [open, setOpen] = createSignal(false); @@ -249,24 +299,30 @@ function createModal() { openModal() { setOpen(true); }, - Modal() { + Modal(props: ModalProps) { return ( - - - Hello from modal - setOpen(false)}>close modal - - + > + + + { + props.setShow(false); + await props.handleVideoInput?.(); + await props.lockPlay?.(); + }}>Continue Playing + setOpen(false)}>Shutdown Nestri + + + ); From b734892c55370bac1e00ea145a2b360dcddf354e Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Sun, 9 Mar 2025 14:53:11 +0100 Subject: [PATCH 10/11] extracted modal component + showing modal on enter and mouspointer loss --- packages/www/src/components/Modal.tsx | 80 ++++++++++ packages/www/src/pages/play.tsx | 208 +++++++++++--------------- 2 files changed, 166 insertions(+), 122 deletions(-) create mode 100644 packages/www/src/components/Modal.tsx diff --git a/packages/www/src/components/Modal.tsx b/packages/www/src/components/Modal.tsx new file mode 100644 index 00000000..5886bbe4 --- /dev/null +++ b/packages/www/src/components/Modal.tsx @@ -0,0 +1,80 @@ +import { Component, JSX, Show, createSignal } from "solid-js"; +import { Portal } from "solid-js/web"; +import { styled } from "@macaron-css/solid"; +import { theme } from "@nestri/www/ui/theme"; + +const ModalContainer = styled("div", { + base: { + width: "100%", + maxWidth: 370, + maxHeight: "75vh", + height: "auto", + borderRadius: 12, + borderWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d400, + backgroundColor: theme.color.gray.d200, + boxShadow: theme.color.boxShadow, + backdropFilter: "blur(20px)", + padding: "20px 25px" + } +}) + +export interface ModalProps { + isOpen?: boolean; + onClose?: ((value: boolean) => void) | (() => void); + children: JSX.Element; + mountPoint?: HTMLElement; + containerClass?: string; + overlayClass?: string; +} + +export function createModalController() { + const [isOpen, setIsOpen] = createSignal(false); + + return { + isOpen, + open: () => setIsOpen(true), + close: () => setIsOpen(false), + toggle: () => setIsOpen(!isOpen()), + }; +} + +export const Modal: Component = (props) => { + const mountPoint = props.mountPoint || document.getElementById("styled") || document.body; + const isOpen = () => props.isOpen ?? false; + + const defaultOverlayStyle = ` + position: fixed; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + z-index: 50; + `; + + return ( + + + { + if (e.target === e.currentTarget && props.onClose) { + if (props.onClose.length > 0) { + (props.onClose as (value: boolean) => void)(false); + } else { + (props.onClose as () => void)(); + } + } + }} + > + + {props.children} + + + + + ); +}; diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 6df0e5ca..91ba5102 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -8,6 +8,7 @@ import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; import { Container, FullScreen } from "@nestri/www/ui/layout"; import { styled } from "@macaron-css/solid"; import { lightClass, theme, darkClass } from "@nestri/www/ui/theme"; +import { Modal, createModalController } from "../components/Modal"; const Canvas = styled("canvas", { base: { @@ -19,22 +20,7 @@ const Canvas = styled("canvas", { } }); -const ModalContainer = styled("div", { - base: { - width: "100%", - maxWidth: 370, - maxHeight: "75vh", - height: "auto", - borderRadius: 12, - borderWidth: 1, - borderStyle: "solid", - borderColor: theme.color.gray.d400, - backgroundColor: theme.color.gray.d200, - boxShadow: theme.color.boxShadow, - backdropFilter: "blur(20px)", - padding: "20px 25px" - } -}) + const Button = styled("button", { base: { @@ -72,6 +58,7 @@ export function PlayComponent() { let nestriMouse: Mouse, nestriKeyboard: Keyboard; const { Modal, openModal } = createModal(); + const { WelcomeModal, openWelcomeModal } = createWelcomeModal(); const initializeInputDevices = () => { const canvasElement = canvas(); @@ -149,14 +136,17 @@ export function PlayComponent() { if (document.pointerLockElement === canvasElement) { initializeInputDevices(); } else { - - if (!showBannerModal) { + console.log("Pointer lock lost Show Banner Modal:", showBannerModal()); + if (!showBannerModal()) { + console.log("Pointer lock lost, showing banner"); const playing = sessionStorage.getItem("showedBanner"); setShowBannerModal(!playing || playing !== "true"); - if(!playing) { + openWelcomeModal(); + + if (playing) { + setShowButtonModal(true); openModal(); } - } @@ -202,11 +192,27 @@ export function PlayComponent() { setupPointerLockListener(); video = document.createElement("video"); video.style.visibility = "hidden"; - webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { + webrtc = new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => { if (video && mediaStream) { video.srcObject = mediaStream; setHasStream(true); setShowOffline(false); + + const playing = sessionStorage.getItem("showedBanner") + console.log("Playing:", playing); + if (!playing || playing != "true") { + console.log("Showing banner: ", showBannerModal()); + if (!showBannerModal()) { + setShowBannerModal(false) + openWelcomeModal(); + } + } else { + if (!showButtonModal()) { + setShowButtonModal(true) + openModal(); + } + } + await handleVideoInput(); } else if (mediaStream === null) { console.log("MediaStream is null, Room is offline"); @@ -229,20 +235,27 @@ export function PlayComponent() { }); return ( - {showOffline() ? ( - - Offline - - ) : ( - - )} + {showOffline() ? ( + + Offline + + ) : ( + + )} + + - + setShow={setShowButtonModal} + closeOnBackdropClick={false} + handleVideoInput={handleVideoInput} + lockPlay={lockPlay} /> + ); } @@ -254,109 +267,60 @@ interface ModalProps { lockPlay?: () => Promise; } +type GameModalProps = ModalProps & { + handleVideoInput?: () => Promise; + lockPlay?: () => Promise; + setShow?: (show: boolean) => void; +} + function createWelcomeModal() { - const [open, setOpen] = createSignal(false); + const controller = createModalController(); return { - openWelcomeModal() { - setOpen(true); - }, - WelcomeModal(props: ModalProps) { + openWelcomeModal: controller.open, + WelcomeModal(props: GameModalProps) { return ( - - - - - - Happy that you use Nestri! - { - sessionStorage.setItem("showedBanner", "true"); - await props.handleVideoInput?.(); - await props.lockPlay?.(); - }}>Let's go - - - - - + + + Happy that you use Nestri! + { + sessionStorage.setItem("showedBanner", "true"); + await props.handleVideoInput?.(); + await props.lockPlay?.(); + controller.close(); + }}>Let's go + + ); }, }; } function createModal() { - const [open, setOpen] = createSignal(false); + const controller = createModalController(); return { - openModal() { - setOpen(true); - }, - Modal(props: ModalProps) { + openModal: controller.open, + Modal(props: GameModalProps) { return ( - - - - - - { - props.setShow(false); - await props.handleVideoInput?.(); - await props.lockPlay?.(); - }}>Continue Playing - setOpen(false)}>Shutdown Nestri - - - - - + + + { + props.setShow?.(false); + await props.handleVideoInput?.(); + await props.lockPlay?.(); + controller.close(); + }}>Continue Playing + Shutdown Nestri + + ); }, }; -} - -function Modal(props: ModalProps) { - return ( - - - e.stopPropagation()} // Prevent closing when clicking inside modal - > - - - { - props.setShow(false); - sessionStorage.setItem("showedBanner", "true"); - await props.handleVideoInput?.(); - await props.lockPlay?.(); - }} - > - Continue Playing - - - Shutdown Nestri - - - - - ); } \ No newline at end of file From 5189bf768a73f5a27d0c31387e909df78efedfd4 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Sat, 15 Mar 2025 21:06:30 +0100 Subject: [PATCH 11/11] merge --- package.json | 23 +- packages/core/package.json | 1 + packages/core/src/examples.ts | 8 +- packages/core/src/polar.ts | 8 - packages/core/src/polar/index.ts | 169 ++++++++++ packages/core/src/polar/polar.sql.ts | 22 ++ packages/core/src/team/index.ts | 23 +- packages/core/src/user/index.ts | 12 +- packages/functions/package.json | 2 +- packages/functions/src/api/account.ts | 8 +- packages/server/Cargo.lock | 6 +- packages/www/index.html | 4 +- packages/www/package.json | 8 +- packages/www/src/App.tsx | 11 +- packages/www/src/pages/default-state.tsx | 7 - packages/www/src/pages/new.tsx | 165 ++++++++- .../www/src/providers/{auth.tsx => auth.ts} | 6 +- packages/www/src/ui/button.ts | 34 ++ packages/www/src/ui/form.tsx | 316 ++++++++++++++++++ packages/www/src/ui/index.ts | 6 + packages/www/src/ui/layout.tsx | 108 ++++-- packages/www/src/ui/text.tsx | 15 +- packages/www/src/ui/theme.ts | 30 +- 23 files changed, 890 insertions(+), 102 deletions(-) delete mode 100644 packages/core/src/polar.ts create mode 100644 packages/core/src/polar/index.ts create mode 100644 packages/core/src/polar/polar.sql.ts delete mode 100644 packages/www/src/pages/default-state.tsx rename packages/www/src/providers/{auth.tsx => auth.ts} (96%) create mode 100644 packages/www/src/ui/button.ts create mode 100644 packages/www/src/ui/form.tsx create mode 100644 packages/www/src/ui/index.ts diff --git a/package.json b/package.json index 407f82af..31c08650 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,5 @@ { "name": "nestri", - "private": true, - "scripts": { - "build": "turbo build", - "dev": "turbo dev", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "lint": "turbo lint", - "sso": "aws sso login --sso-session=nestri --no-browser --use-device-code" - }, "devDependencies": { "@cloudflare/workers-types": "4.20240821.1", "@pulumi/pulumi": "^3.134.0", @@ -18,16 +10,21 @@ "engines": { "node": ">=18" }, - "packageManager": "bun@1.1.18", - "workspaces": [ - "apps/*", - "packages/*" - ], + "packageManager": "bun@1.2.4", + "private": true, + "scripts": { + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "sso": "aws sso login --sso-session=nestri --no-browser --use-device-code" + }, "trustedDependencies": [ "core-js-pure", "esbuild", "workerd" ], + "workspaces": [ + "apps/*", + "packages/*" + ], "dependencies": { "sst": "3.9.1" } diff --git a/packages/core/package.json b/packages/core/package.json index 67488454..5e7cd610 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ "@aws-sdk/client-sesv2": "^3.753.0", "@instantdb/admin": "^0.17.7", "@neondatabase/serverless": "^0.10.4", + "@openauthjs/openauth": "0.4.3", "@openauthjs/openevent": "^0.0.27", "@polar-sh/sdk": "^0.26.1", "drizzle-orm": "^0.39.3", diff --git a/packages/core/src/examples.ts b/packages/core/src/examples.ts index bb59bb93..793ac7f3 100644 --- a/packages/core/src/examples.ts +++ b/packages/core/src/examples.ts @@ -1,4 +1,3 @@ -import { teamID } from "./drizzle/types"; import { prefixes } from "./utils"; export module Examples { export const Id = (prefix: keyof typeof prefixes) => @@ -18,7 +17,7 @@ export module Examples { name: "John Does' Team", slug: "john_doe", } - + export const Member = { id: Id("member"), email: "john@example.com", @@ -26,4 +25,9 @@ export module Examples { timeSeen: new Date("2025-02-23T13:39:52.249Z"), } + export const Polar = { + teamID: Id("team"), + timeSeen: new Date("2025-02-23T13:39:52.249Z"), + } + } \ No newline at end of file diff --git a/packages/core/src/polar.ts b/packages/core/src/polar.ts deleted file mode 100644 index 660aff52..00000000 --- a/packages/core/src/polar.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Resource } from "sst"; -import { Polar as PolarSdk } from "@polar-sh/sdk"; - -const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" }); - -export module Polar { - export const client = polar; -} \ No newline at end of file diff --git a/packages/core/src/polar/index.ts b/packages/core/src/polar/index.ts new file mode 100644 index 00000000..36c214a6 --- /dev/null +++ b/packages/core/src/polar/index.ts @@ -0,0 +1,169 @@ +import { z } from "zod"; +import { fn } from "../utils"; +import { Resource } from "sst"; +import { eq, and } from "../drizzle"; +import { useTeam } from "../actor"; +import { createEvent } from "../event"; +import { polarTable, Standing } from "./polar.sql"; +import { Polar as PolarSdk } from "@polar-sh/sdk"; +import { useTransaction } from "../drizzle/transaction"; + +const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" }); + +export module Polar { + export const client = polar; + + export const Info = z.object({ + teamID: z.string(), + customerID: z.string(), + subscriptionID: z.string().nullable(), + subscriptionItemID: z.string().nullable(), + standing: z.enum(Standing), + }); + + export type Info = z.infer; + + export const Checkout = z.object({ + annual: z.boolean().optional(), + successUrl: z.string(), + cancelUrl: z.string(), + }); + + export const CheckoutSession = z.object({ + url: z.string().nullable(), + }); + + export const CustomerSubscriptionEventType = [ + "created", + "updated", + "deleted", + ] as const; + + export const Events = { + CustomerSubscriptionEvent: createEvent( + "polar.customer-subscription-event", + z.object({ + type: z.enum(CustomerSubscriptionEventType), + status: z.string(), + teamID: z.string().min(1), + customerID: z.string().min(1), + subscriptionID: z.string().min(1), + subscriptionItemID: z.string().min(1), + }), + ), + }; + + export function get() { + return useTransaction(async (tx) => + tx + .select() + .from(polarTable) + .where(eq(polarTable.teamID, useTeam())) + .execute() + .then((rows) => rows.map(serialize).at(0)), + ); + } + + export const fromUserEmail = fn(z.string().min(1), async (email) => { + try { + const customers = await client.customers.list({ email }) + + if (customers.result.items.length === 0) { + return await client.customers.create({ email }) + } else { + return customers.result.items[0] + } + + } catch (err) { + //FIXME: This is the issue [Polar.sh/#5147](https://github.com/polarsource/polar/issues/5147) + // console.log("error", err) + return undefined + } + }) + + export const setCustomerID = fn(Info.shape.customerID, async (customerID) => + useTransaction(async (tx) => + tx + .insert(polarTable) + .values({ + teamID: useTeam(), + customerID, + standing: "new", + }) + .execute(), + ), + ); + + export const setSubscription = fn( + Info.pick({ + subscriptionID: true, + subscriptionItemID: true, + }), + (input) => + useTransaction(async (tx) => + tx + .update(polarTable) + .set({ + subscriptionID: input.subscriptionID, + subscriptionItemID: input.subscriptionItemID, + }) + .where(eq(polarTable.teamID, useTeam())) + .returning() + .execute() + .then((rows) => rows.map(serialize).at(0)), + ), + ); + + export const removeSubscription = fn( + z.string().min(1), + (stripeSubscriptionID) => + useTransaction((tx) => + tx + .update(polarTable) + .set({ + subscriptionItemID: null, + subscriptionID: null, + }) + .where(and(eq(polarTable.subscriptionID, stripeSubscriptionID))) + .execute(), + ), + ); + + export const setStanding = fn( + Info.pick({ + subscriptionID: true, + standing: true, + }), + (input) => + useTransaction((tx) => + tx + .update(polarTable) + .set({ standing: input.standing }) + .where(and(eq(polarTable.subscriptionID, input.subscriptionID!))) + .execute(), + ), + ); + + export const fromCustomerID = fn(Info.shape.customerID, (customerID) => + useTransaction((tx) => + tx + .select() + .from(polarTable) + .where(and(eq(polarTable.customerID, customerID))) + .execute() + .then((rows) => rows.map(serialize).at(0)), + ), + ); + + function serialize( + input: typeof polarTable.$inferSelect, + ): z.infer { + return { + teamID: input.teamID, + customerID: input.customerID, + subscriptionID: input.subscriptionID, + subscriptionItemID: input.subscriptionItemID, + standing: input.standing, + }; + } +} \ No newline at end of file diff --git a/packages/core/src/polar/polar.sql.ts b/packages/core/src/polar/polar.sql.ts new file mode 100644 index 00000000..6e28748c --- /dev/null +++ b/packages/core/src/polar/polar.sql.ts @@ -0,0 +1,22 @@ +import { timestamps, teamID } from "../drizzle/types"; +import { teamIndexes, teamTable } from "../team/team.sql"; +import { pgTable, text, varchar } from "drizzle-orm/pg-core"; + +export const Standing = ["new", "good", "overdue"] as const; + +export const polarTable = pgTable( + "polar", + { + teamID: teamID.teamID.primaryKey().references(() => teamTable.id), + ...timestamps, + customerID: varchar("customer_id", { length: 255 }).notNull(), + subscriptionID: varchar("subscription_id", { length: 255 }), + subscriptionItemID: varchar("subscription_item_id", { + length: 255, + }), + standing: text("standing", { enum: Standing }).notNull(), + }, + (table) => ({ + ...teamIndexes(table), + }) +) \ No newline at end of file diff --git a/packages/core/src/team/index.ts b/packages/core/src/team/index.ts index 1db157bc..3d7a882e 100644 --- a/packages/core/src/team/index.ts +++ b/packages/core/src/team/index.ts @@ -3,13 +3,13 @@ import { Resource } from "sst"; import { bus } from "sst/aws/bus"; import { Common } from "../common"; import { createID, fn } from "../utils"; -import { VisibleError } from "../error"; import { Examples } from "../examples"; import { teamTable } from "./team.sql"; import { createEvent } from "../event"; -import { assertActor } from "../actor"; +import { assertActor, withActor } from "../actor"; import { and, eq, sql } from "../drizzle"; import { memberTable } from "../member/member.sql"; +import { HTTPException } from 'hono/http-exception'; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; export module Team { @@ -45,11 +45,11 @@ export module Team { ), }; - export class WorkspaceExistsError extends VisibleError { + export class TeamExistsError extends HTTPException { constructor(slug: string) { super( - "team.slug_exists", - `there is already a workspace named "${slug}"`, + 400, + { message: `There is already a team named "${slug}"`, } ); } } @@ -65,15 +65,16 @@ export module Team { slug: input.slug, name: input.name }) - .onConflictDoNothing() - .returning({ insertedID: teamTable.id }) + .onConflictDoNothing({ target: teamTable.slug }) - if (result.length === 0) throw new WorkspaceExistsError(input.slug); + if (!result.rowCount) throw new TeamExistsError(input.slug); await afterTx(() => - bus.publish(Resource.Bus, Events.Created, { - teamID: id, - }), + withActor({ type: "system", properties: { teamID: id } }, () => + bus.publish(Resource.Bus, Events.Created, { + teamID: id, + }) + ), ); return id; }) diff --git a/packages/core/src/user/index.ts b/packages/core/src/user/index.ts index 439e1590..a27f1ad2 100644 --- a/packages/core/src/user/index.ts +++ b/packages/core/src/user/index.ts @@ -1,4 +1,6 @@ import { z } from "zod"; +import { Polar } from "../polar"; +import { Team } from "../team"; import { bus } from "sst/aws/bus"; import { Common } from "../common"; import { createID, fn } from "../utils"; @@ -11,7 +13,6 @@ import { assertActor, withActor } from "../actor"; import { memberTable } from "../member/member.sql"; import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle"; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; -import { Team } from "../team"; export module User { @@ -106,12 +107,8 @@ export module User { //FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake - // const customer = await Polar.client.customers.create({ - // email: input.email, - // metadata: { - // userID, - // }, - // }); + const customer = await Polar.fromUserEmail(input.email) + console.log("customer", customer) const name = sanitizeUsername(input.name); @@ -131,6 +128,7 @@ export module User { avatarUrl: input.avatarUrl, email: input.email, discriminator: Number(discriminator), + polarCustomerID: customer?.id }) await afterTx(() => withActor({ diff --git a/packages/functions/package.json b/packages/functions/package.json index 29ed020f..88e2baa4 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -14,7 +14,7 @@ "typescript": "^5.0.0" }, "dependencies": { - "@openauthjs/openauth": "^0.3.9", + "@openauthjs/openauth": "0.4.3", "hono": "^4.6.15", "hono-openapi": "^0.3.1", "partysocket": "1.0.3" diff --git a/packages/functions/src/api/account.ts b/packages/functions/src/api/account.ts index 32ce1de7..c5ab6163 100644 --- a/packages/functions/src/api/account.ts +++ b/packages/functions/src/api/account.ts @@ -42,8 +42,10 @@ export module AccountApi { }), async (c) => { const actor = assertActor("user"); - const currentUser = await User.fromID(actor.properties.userID) - if (!currentUser) return c.json({ error: "This account does not exist, it may have been deleted" }, 404) + const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()]) + + if (!currentUser) return c.json({ error: "This account does not exist; it may have been deleted" }, 404) + const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser return c.json({ @@ -51,10 +53,10 @@ export module AccountApi { id, email, name, + teams, avatarUrl, discriminator, polarCustomerID, - teams: await User.teams(), } }, 200); }, diff --git a/packages/server/Cargo.lock b/packages/server/Cargo.lock index eca99bbb..579948cc 100644 --- a/packages/server/Cargo.lock +++ b/packages/server/Cargo.lock @@ -1876,7 +1876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -2668,9 +2668,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", diff --git a/packages/www/index.html b/packages/www/index.html index 3ec7a691..536f6e06 100644 --- a/packages/www/index.html +++ b/packages/www/index.html @@ -9,12 +9,12 @@ { ( - // - // {props.children} - props.children - // + + {props.children} + + // props.children )} > - + { diff --git a/packages/www/src/pages/default-state.tsx b/packages/www/src/pages/default-state.tsx deleted file mode 100644 index c13befc0..00000000 --- a/packages/www/src/pages/default-state.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function DefaultState() { - return ( - - We are logging you in - - ) -} \ No newline at end of file diff --git a/packages/www/src/pages/new.tsx b/packages/www/src/pages/new.tsx index 3319de5b..4b926763 100644 --- a/packages/www/src/pages/new.tsx +++ b/packages/www/src/pages/new.tsx @@ -1,15 +1,168 @@ +import * as v from "valibot" +import { styled } from "@macaron-css/solid"; import { Text } from "@nestri/www/ui/text"; +import { utility } from "@nestri/www/ui/utility"; +import { theme } from "@nestri/www/ui/theme"; +import { FormField, Input, Select } from "@nestri/www/ui/form"; import { Container, FullScreen } from "@nestri/www/ui/layout"; +import { createForm, required, email, valiForm } from "@modular-forms/solid"; +import { Button } from "@nestri/www/ui"; + +// const nameRegex = /^[a-z]+$/ + +const FieldList = styled("div", { + base: { + width: "100%", + maxWidth: 380, + ...utility.stack(5), + }, +}); + +const Hr = styled("hr", { + base: { + border: 0, + backgroundColor: theme.color.gray.d400, + width: "100%", + height: 1, + } +}) + +const Plan = { + Pro: 'BYOG', + Basic: 'Hosted', +} as const; + +const schema = v.object({ + plan: v.pipe( + v.enum(Plan), + v.minLength(2,"Please choose a plan"), + ), + display_name: v.pipe( + v.string(), + v.maxLength(32, 'Please use 32 characters at maximum.'), + ), + slug: v.pipe( + v.string(), + v.minLength(2, 'Please use 2 characters at minimum.'), + // v.regex(nameRegex, "Use only small letters, no numbers or special characters"), + v.maxLength(48, 'Please use 48 characters at maximum.'), + ) +}) + +// const Details = styled("details", { +// base: { +// overflow: "hidden", +// transition: "max-height .2s ease" +// } +// }) + +// const Summary = styled("summary", { +// base: { +// userSelect: "none", +// cursor: "pointer", +// listStyle: "none" +// } +// }) + +// const SVG = styled("svg", { +// base: { +// color: theme.color.gray.d900, +// width: 20, +// height: 20, +// marginRight: theme.space[2] +// } +// }) + +// const Subtitle = styled("p", { +// base: { +// color: theme.color.gray.d900, +// fontSize: theme.font.size.sm, +// fontWeight: theme.font.weight.regular, +// lineHeight: "1rem" +// } +// }) + +export function CreateTeamComponent() { + const [form, { Form, Field }] = createForm({ + validate: valiForm(schema), + }); -export function TeamCreate() { return ( - - - Your first deploy is just a sign-up away. - - + + + + Create a Team + + + Choose something that your teammates will recognize + + + + + + + {(field, props) => ( + + + + )} + + + {(field, props) => ( + + + + )} + + {/* + + + + + Continuing will start a 14-day Pro plan trial. + + + + */} + + Continue + + + + ) } \ No newline at end of file diff --git a/packages/www/src/providers/auth.tsx b/packages/www/src/providers/auth.ts similarity index 96% rename from packages/www/src/providers/auth.tsx rename to packages/www/src/providers/auth.ts index bfe03a52..4427d008 100644 --- a/packages/www/src/providers/auth.tsx +++ b/packages/www/src/providers/auth.ts @@ -23,6 +23,8 @@ interface Storage { current?: string; } +//TODO: Fix bug where authenticator deletes auth state for no reason + export const client = createClient({ issuer: import.meta.env.VITE_AUTH_URL, clientID: "web", @@ -83,6 +85,7 @@ export const { use: useAuth, provider: AuthProvider } = access: account.access, }) if (result.err) { + console.log("error", result.err) if ("id" in account) setStore(produce((state) => { delete state.accounts[account.id]; @@ -98,7 +101,7 @@ export const { use: useAuth, provider: AuthProvider } = authorization: `Bearer ${tokens.access}`, }, }).then(async (response) => { - await new Promise((resolve) => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 10000)); if (response.ok) { const result = await response.json(); @@ -115,6 +118,7 @@ export const { use: useAuth, provider: AuthProvider } = } if (!response.ok) + console.log("error from account", response.json()) setStore( produce((state) => { delete state.accounts[account.id]; diff --git a/packages/www/src/ui/button.ts b/packages/www/src/ui/button.ts new file mode 100644 index 00000000..afb63a9d --- /dev/null +++ b/packages/www/src/ui/button.ts @@ -0,0 +1,34 @@ +import { theme } from "./theme"; +import { styled } from "@macaron-css/solid"; + +export const Button = styled("button", { + base: { + borderRadius: 6, + border: "1px solid transparent", + padding: `${theme.space[2]} ${theme.space[4]}`, + fontWeight: 500, + letterSpacing: 0.1, + lineHeight: "normal", + fontFamily: theme.font.family.heading, + textAlign: "center", + transitionDelay: "0s, 0s", + transitionDuration: "0.2s, 0.2s", + transitionProperty: "background-color, border", + transitionTimingFunction: "ease-out, ease-out", + display: "inline-flex", + gap: theme.space[1.5], + alignItems: "center", + justifyContent: "center", + ":disabled": { + pointerEvents: "none", + }, + }, + variants: { + color: { + brand: { + backgroundColor: theme.color.brand, + color: "#FFF", + } + } + } +}) \ No newline at end of file diff --git a/packages/www/src/ui/form.tsx b/packages/www/src/ui/form.tsx new file mode 100644 index 00000000..ed63c0d7 --- /dev/null +++ b/packages/www/src/ui/form.tsx @@ -0,0 +1,316 @@ +import { theme } from "./theme"; +import { styled } from "@macaron-css/solid" +import { CSSProperties } from "@macaron-css/core"; +import { ComponentProps, createMemo, For, JSX, Show, splitProps } from "solid-js"; +import { Container } from "./layout"; +import { utility } from "./utility"; + +// FIXME: Make sure the focus ring goes to red when the input is invalid + +export const inputStyles: CSSProperties = { + lineHeight: theme.font.lineHeight, + appearance: "none", + fontSize: theme.font.size.sm, + borderRadius: theme.borderRadius, + padding: `0 ${theme.space[3]}`, + height: theme.input.size.base, + borderWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d400, + color: theme.color.d1000.gray, + backgroundColor: theme.color.background.d100, +}; + +export const inputDisabledStyles: CSSProperties = { + opacity: 0.5, + backgroundColor: theme.color.background.d200, + color: theme.color.gray.d400, + cursor: "default", + // boxShadow: `0 0 0 1px inset ${theme.color.input.border}`, +}; + +export const inputFocusStyles: CSSProperties = { + outlineOffset: 3, + outline: `${theme.color.gray.d600} solid 2px`, +}; + +export const inputDangerTextStyles: CSSProperties = { + color: theme.color.red.d700, +}; + +export const inputDangerFocusStyles: CSSProperties = { + ...inputDangerTextStyles, + outlineColor: theme.color.red.d700, + // boxShadow: ` + // 0 0 1px 1px inset hsla(${theme.color.red.l2}, 100%), + // ${theme.color.input.shadow} + // `, +}; + +export const Root = styled("label", { + base: { + ...utility.stack(2), + }, + variants: { + color: { + primary: { + color: theme.color.gray.d900 + }, + danger: { + color: theme.color.red.d900, + // selectors: { + // "&:has(input)": { + // ...inputDangerFocusStyles + // } + // } + }, + }, + }, + defaultVariants: { + color: "primary", + }, +}); + +type FormFieldProps = ComponentProps & { + hint?: JSX.Element; + label?: string; +}; + +export const Input = styled("input", { + base: { + ...inputStyles, + ":focus": { + ...inputFocusStyles, + }, + ":disabled": { + ...inputDisabledStyles, + }, + "::placeholder": { + color: theme.color.gray.d800 + }, + // ":invalid":{ + // ...inputDangerFocusStyles + // }, + // selectors: { + // [`${Root.selector({ color: "danger" })} &`]: { + // ...inputDangerFocusStyles, + // }, + // }, + }, + variants: { + color: { + primary: {}, + danger: { + ...inputDangerFocusStyles, + ":focus": { + ...inputDangerFocusStyles, + }, + }, + }, + size: { + sm: { + height: theme.input.size.sm, + }, + }, + }, + defaultVariants: { + color: "primary", + }, +}); + +export const InputRadio = styled("input", { + base: { + padding: 0, + // borderRadius: 0, + WebkitAppearance: "none", + appearance: "none", + /* For iOS < 15 to remove gradient background */ + backgroundColor: theme.color.background.d100, + /* Not removed via appearance */ + margin: 0, + font: "inherit", + color: "currentColor", + width: "1.15em", + height: "1.15em", + border: "0.15em solid currentColor", + borderRadius: "50%", + transform: "translateY(-0.075em)", + display: "grid", + position: "relative", + placeContent: "center", + ":before": { + content: "", + width: "0.68em", + height: "0.68em", + borderRadius: "50%", + transform: " scale(0)", + transition: "120ms transform ease-in-out", + boxShadow: `inset 1em 1em ${theme.color.blue.d700}` + }, + selectors: { + "&:checked::before": { + transform: "scale(1)" + } + } + } +}); + +const Label = styled("p", { + base: { + fontWeight: 500, + letterSpacing: -0.1, + fontSize: theme.font.size.mono_sm, + textTransform: "capitalize", + fontFamily: theme.font.family.heading, + }, +}); + +const InputLabel = styled("label", { + base: { + letterSpacing: -0.1, + fontSize: theme.font.size.sm, + lineHeight: theme.font.lineHeight, + height: theme.input.size.base, + appearance: "none", + padding: `0 ${theme.space[3]}`, + borderWidth: 0, + borderBottomWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d400, + color: theme.color.gray.d800, + backgroundColor: theme.color.background.d100, + position: "relative", + display: "flex", + alignItems: "center", + cursor: "pointer", + gap: "1em", + ":focus-within": { + color: theme.color.d1000.gray + }, + ":first-child": { + borderTopRightRadius: theme.borderRadius, + borderTopLeftRadius: theme.borderRadius, + }, + ":last-child": { + borderBottomWidth: 0, + borderBottomRightRadius: theme.borderRadius, + borderBottomLeftRadius: theme.borderRadius, + }, + ":hover": { + backgroundColor: theme.color.background.d200, + }, + selectors: { + "&:has(input:checked)": { + color: theme.color.d1000.gray + } + } + }, +}); + +const Hint = styled("p", { + base: { + fontSize: theme.font.size.xs, + lineHeight: theme.font.lineHeight, + color: theme.color.gray.d800, + }, + variants: { + color: { + primary: {}, + danger: { + color: theme.color.red.d700, + }, + }, + }, + defaultVariants: { + color: "primary", + }, +}); + +export function FormField(props: FormFieldProps) { + return ( + + + + {props.label} + + {props.children} + + + {props.hint!} + + + ); +} + +type SelectProps = { + ref: (element: HTMLInputElement) => void; + name: string; + value: any; + onInput: JSX.EventHandler; + onChange: JSX.EventHandler; + onBlur: JSX.EventHandler; + options: { label: string; value: string }[]; + badges?: { label: string; color: keyof typeof theme.color.d1000 }[]; + required?: boolean; + class?: string; +}; + +const InputRadioContainer = styled("div", { + base: { + ...inputStyles, + display: "flex", + userSelect: "none", + flexDirection: "column", + height: "auto", + position: "relative", + padding: 0, + } +}) + +const Badge = styled("div", { + base: { + color: "#FFF", + marginLeft: "auto", + borderRadius: 9999, + letterSpacing: 0.5, + padding: "0 6px", + fontSize: theme.font.size.xs + } + +}) + +export function Select(props: SelectProps) { + // Split select element props + const [, inputProps] = splitProps(props, [ + 'class', + 'value', + 'options', + 'badges', + ]); + + return ( + + + {({ label, value }, key) => ( + + + {label} + + {props.badges && + + {props.badges[key()].label} + + } + + + )} + + + + ); +} \ No newline at end of file diff --git a/packages/www/src/ui/index.ts b/packages/www/src/ui/index.ts new file mode 100644 index 00000000..a5f93846 --- /dev/null +++ b/packages/www/src/ui/index.ts @@ -0,0 +1,6 @@ +export * from "./form" +export * from "./layout" +export * from "./text" +export * from "./theme" +export * from "./utility" +export * from "./button" \ No newline at end of file diff --git a/packages/www/src/ui/layout.tsx b/packages/www/src/ui/layout.tsx index d22dc083..8af6e181 100644 --- a/packages/www/src/ui/layout.tsx +++ b/packages/www/src/ui/layout.tsx @@ -4,7 +4,6 @@ import { styled } from "@macaron-css/solid"; export const FullScreen = styled("div", { base: { inset: 0, - zIndex: 0, display: "flex", position: "fixed", alignItems: "center", @@ -21,28 +20,97 @@ export const FullScreen = styled("div", { }, }) +// export const Container = styled("div", { +// base: { +// backgroundColor: theme.color.background.d100, +// borderColor: theme.color.gray.d400, +// padding: "64px 80px 48px", +// justifyContent: "center", +// borderStyle: "solid", +// position: "relative", +// borderRadius: 12, +// alignItems: "center", +// maxWidth: 550, +// borderWidth: 1, +// display: "flex", +// }, +// variants: { +// flow: { +// column: { +// flexDirection: "column" +// }, +// row: { +// flexDirection: "row" +// } +// } +// } +// }) + export const Container = styled("div", { base: { - backgroundColor: theme.color.background.d100, - borderColor: theme.color.gray.d400, - padding: "64px 80px 48px", - justifyContent: "center", - borderStyle: "solid", - position: "relative", - borderRadius: 12, - alignItems: "center", - maxWidth: 550, - borderWidth: 1, display: "flex", + flexDirection: "column", }, variants: { - flow: { - column: { - flexDirection: "column" - }, - row: { - flexDirection: "row" + space: (() => { + const result = {} as Record<`${keyof (typeof theme)["space"]}`, any>; + for (const key in theme.space) { + const value = theme.space[key as keyof typeof theme.space]; + result[key as keyof typeof theme.space] = { + gap: value, + }; + } + return result; + })(), + rounded: (() => { + const result = {} as Record<`${keyof (typeof theme)["space"]}`, any>; + for (const key in theme.space) { + const value = theme.space[key as keyof typeof theme.space]; + result[key as keyof typeof theme.space] = { + borderRadius: value, + }; } - } - } -}) \ No newline at end of file + return result; + })(), + highlighted: { + true: { + borderColor: theme.color.gray.d400, + backgroundColor: theme.color.background.d100, + borderStyle: "solid", + borderWidth: 1, + padding: "64px 80px 48px", + maxWidth: 550, + } + }, + flex: { + true: { + flex: "1 1 auto", + }, + false: { + flex: "0 0 auto", + }, + }, + horizontal: { + center: { + alignItems: "center", + }, + start: { + alignItems: "flex-start", + }, + end: { + alignItems: "flex-end", + }, + }, + vertical: { + center: { + justifyContent: "center", + }, + start: { + justifyContent: "flex-start", + }, + end: { + justifyContent: "flex-end", + }, + }, + }, +}); \ No newline at end of file diff --git a/packages/www/src/ui/text.tsx b/packages/www/src/ui/text.tsx index 93c332c6..6cb7b2b9 100644 --- a/packages/www/src/ui/text.tsx +++ b/packages/www/src/ui/text.tsx @@ -4,9 +4,9 @@ import { utility } from "./utility"; import { CSSProperties } from "@macaron-css/core"; export const Text = styled("span", { - base: { - textWrap: "balance" - }, + // base: { + // textWrap: "balance" + // }, variants: { leading: { base: { @@ -122,6 +122,15 @@ export const Text = styled("span", { } return result; })(), + font: (() => { + const result = {} as Record<`${keyof typeof theme.font.family}`, any>; + for (const [key, value] of Object.entries(theme.font.family)) { + result[key as keyof typeof theme.font.family] = { + fontFamily: value, + }; + } + return result; + })(), color: (() => { const record = {} as Record; for (const [key, _value] of Object.entries(theme.color.text)) { diff --git a/packages/www/src/ui/theme.ts b/packages/www/src/ui/theme.ts index 5f6fbddd..e2bb15ba 100644 --- a/packages/www/src/ui/theme.ts +++ b/packages/www/src/ui/theme.ts @@ -2,7 +2,7 @@ import { createTheme } from "@macaron-css/core"; const constants = { colorFadeDuration: "0.15s", - borderRadius: "4px", + borderRadius: "6px", textBoldWeight: "600", iconOpacity: "0.85", modalWidth: { @@ -16,6 +16,13 @@ const constants = { }, }; +const formInput = { + size: { + base: "40px", + sm: "32px", + }, + }; + const space = { px: "1px", 0: "0px", @@ -96,7 +103,7 @@ const light = (() => { d100: 'hsla(0,0%,95%)', d200: 'hsla(0,0%,92%)', d300: 'hsla(0,0%,90%)', - d400: 'hsla(0,0%,92%)', + d400: 'hsla(0,0%,82%)', d500: 'hsla(0,0%,79%)', d600: 'hsla(0,0%,66%)', d700: 'hsla(0,0%,56%)', @@ -206,8 +213,9 @@ const light = (() => { teal: "hsla(171,80%,13%)", purple: "hsla(276,100%,15)", pink: "hsla(333,74%,15%)", - grayAlpha: " hsla(0,0%,0%,0.91)" + grayAlpha: " hsla(0,0%,0%,0.91)", } + const brand = "#FF4F01" const background = { d100: '#f5f5f5', @@ -249,8 +257,9 @@ const light = (() => { focusBorder, focusColor, d1000, - text, - boxShadow + boxShadow, + brand, + text }; })() @@ -367,14 +376,16 @@ const dark = (() => { teal: "hsla(166,71%,93%)", purple: "hsla(281,73%,96%)", pink: "hsla( 333,90%,96%)", - grayAlpha: "hsla(0,0%,100%,0.92)" + grayAlpha: "hsla(0,0%,100%,0.92)", } + const brand = "#FF4F01" + const background = { d200: '#171717', d100: "oklch(from #171717 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)" }; - + const contrastFg = '#ffffff'; const focusBorder = `0 0 0 1px ${grayAlpha.d600}, 0px 0px 0px 4px rgba(255,255,255,0.24)`; const focusColor = blue.d900 @@ -412,7 +423,8 @@ const dark = (() => { focusColor, d1000, text, - boxShadow + boxShadow, + brand }; })() @@ -421,6 +433,7 @@ export const [lightClass, theme] = createTheme({ space, font, color: light, + input: formInput }); export const darkClass = createTheme(theme, { @@ -429,4 +442,5 @@ export const darkClass = createTheme(theme, { space, font, color: dark, + input: formInput }); \ No newline at end of file