⭐ feat: Update website, API, and infra (#164)
>Adds `maitred` in charge of handling automated game installs, updates,
and even execution.
>Not only that, we have the hosted stuff here
>- [x] AWS Task on ECS GPUs
>- [ ] Add a service to listen for game starts and stops
(docker-compose.yml)
>- [x] Add a queue for requesting a game to start
>- [x] Fix up the play/watch UI
>TODO:
>- Add a README
>- Add an SST docs
Edit:
- This adds a new landing page, updates the homepage etc etc
>I forgot what the rest of the updated stuff are 😅
57
apps/docs/sst-env.d.ts
vendored
@@ -2,57 +2,8 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"Auth": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"AuthFingerprintKey": {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"CloudflareAuthKV": {
|
||||
"type": "sst.cloudflare.Kv"
|
||||
}
|
||||
"DiscordClientID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"DiscordClientSecret": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GithubClientID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GithubClientSecret": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAdminToken": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAppId": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"LoopsApiKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
export {}
|
||||
@@ -35,10 +35,12 @@
|
||||
"@builder.io/qwik": "^1.8.0",
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@fontsource-variable/bricolage-grotesque": "^5.1.1",
|
||||
"@fontsource-variable/mona-sans": "^5.0.1",
|
||||
"@modular-forms/qwik": "^0.29.0",
|
||||
"@nestri/input": "*",
|
||||
"@nestri/libmoq": "*",
|
||||
"@nestri/sdk": "0.1.0-alpha.11",
|
||||
"@nestri/sdk": "0.1.0-alpha.14",
|
||||
"@nestri/ui": "*",
|
||||
"@openauthjs/openauth": "^0.2.6",
|
||||
"@polar-sh/checkout": "^0.1.8",
|
||||
|
||||
BIN
apps/www/public/fonts/BasementGrotesque-Black.otf
Normal file
BIN
apps/www/public/fonts/BasementGrotesque-Black.woff
Normal file
BIN
apps/www/public/fonts/BasementGrotesque-Black.woff2
Normal file
BIN
apps/www/public/images/avatars/dathorse.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
apps/www/public/images/avatars/janried.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
apps/www/public/images/avatars/victor.png
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
apps/www/public/images/avatars/wanjohi.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
apps/www/public/images/screenshots/doom.png
Normal file
|
After Width: | Height: | Size: 867 KiB |
BIN
apps/www/public/images/screenshots/main-dark.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
apps/www/public/images/screenshots/main-light.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
apps/www/public/images/screenshots/movie.avifs
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
apps/www/public/images/screenshots/multiplayer.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@@ -27,6 +27,18 @@ export default component$(() => {
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f5f5f5" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#171717" />
|
||||
<meta charset="utf-8" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/BasementGrotesque-Black.woff2"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/BasementGrotesque-Black.woff"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
{!isDev && (
|
||||
<link
|
||||
rel="manifest"
|
||||
|
||||
98
apps/www/src/routes/(play)/(user)/home/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import Nestri from "@nestri/sdk";
|
||||
import { server$ } from "@builder.io/qwik-city";
|
||||
import { $, component$ } from "@builder.io/qwik";
|
||||
import { HomeFriendsSection, HomeGamesSection, HomeMachineSection } from "@nestri/ui";
|
||||
|
||||
export const getUserSubscriptions = server$(
|
||||
async function () {
|
||||
|
||||
const access = this.cookie.get("access_token")
|
||||
if (access) {
|
||||
const bearerToken = access.value
|
||||
|
||||
const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
|
||||
const subscriptions = await nestriClient.subscriptions.list().then(t => t.data.length > 0 ? "Pro" : "Free").catch(async (err) => {
|
||||
if (err instanceof Nestri.APIError) {
|
||||
if (err.status == 404) {
|
||||
return "Free"
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
|
||||
return subscriptions as "Free" | "Pro"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getActiveUsers = server$(
|
||||
async function () {
|
||||
|
||||
const access = this.cookie.get("access_token")
|
||||
if (access) {
|
||||
const bearerToken = access.value
|
||||
|
||||
try {
|
||||
const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
|
||||
const users = await nestriClient.users.list().then(t => t.data) as any
|
||||
return users as Nestri.Users.UserListResponse.Data[]
|
||||
} catch (error) {
|
||||
console.log("error", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getSession = server$(
|
||||
async function (profileID: string) {
|
||||
|
||||
const access = this.cookie.get("access_token")
|
||||
if (access) {
|
||||
const bearerToken = access.value
|
||||
try {
|
||||
const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
|
||||
const session = await nestriClient.users.session(profileID)
|
||||
return session
|
||||
} catch (error) {
|
||||
console.log("error", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const createSession = server$(
|
||||
async function () {
|
||||
const access = this.cookie.get("access_token")
|
||||
if (access) {
|
||||
const bearerToken = access.value
|
||||
|
||||
try {
|
||||
const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
|
||||
const taskID = await nestriClient.tasks.create()
|
||||
const sessionID = await nestriClient.tasks.session(taskID.data)
|
||||
return sessionID.data
|
||||
} catch (error) {
|
||||
console.log("error", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
return (
|
||||
<main class="flex w-screen h-full flex-col relative">
|
||||
<section class="max-w-[750px] w-full mx-auto flex flex-col gap-3 px-5 pt-20 pb-14 min-h-screen">
|
||||
<HomeMachineSection getUserSubscription$={$(async () => { return await getUserSubscriptions() })} />
|
||||
<HomeFriendsSection getActiveUsers$={$(async () => { return await getActiveUsers() })} getSession$={$(async (profileID: string) => { return await getSession(profileID) })} />
|
||||
<HomeGamesSection getUserSubscription$={$(async () => { return await getUserSubscriptions() })} createSession$={$(async () => { return await createSession() })} />
|
||||
</section >
|
||||
</main >
|
||||
)
|
||||
})
|
||||
42
apps/www/src/routes/(play)/(user)/layout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import Nestri from "@nestri/sdk";
|
||||
import { HomeNavBar } from "@nestri/ui";
|
||||
import { server$ } from "@builder.io/qwik-city";
|
||||
import { $, component$, Slot } from "@builder.io/qwik"
|
||||
|
||||
const cookieOptions = {
|
||||
path: "/",
|
||||
sameSite: "lax" as const,
|
||||
secure: false, // Only send cookies over HTTPS
|
||||
//FIXME: This causes weird issues in Qwik
|
||||
httpOnly: true, // Prevent JavaScript access to cookies
|
||||
expires: new Date(Date.now() + 24 * 10 * 60 * 60 * 1000), // expires in like 10 days
|
||||
}
|
||||
export const getUserProfile = server$(
|
||||
async function () {
|
||||
const userData = this.cookie.get("profile_data")
|
||||
if (userData) {
|
||||
const user = userData.json() as Nestri.Users.UserRetrieveResponse.Data
|
||||
return user
|
||||
} else {
|
||||
const access = this.cookie.get("access_token")
|
||||
if (access) {
|
||||
const bearerToken = access.value
|
||||
|
||||
const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
|
||||
const currentProfile = await nestriClient.users.retrieve()
|
||||
this.cookie.set("profile_data", JSON.stringify(currentProfile.data), cookieOptions)
|
||||
return currentProfile.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<HomeNavBar getUserProfile$={$(async () => { return await getUserProfile() })} />
|
||||
<Slot />
|
||||
</>
|
||||
)
|
||||
})
|
||||
98
apps/www/src/routes/(play)/(user)/machine/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { component$ } from "@builder.io/qwik"
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
return (
|
||||
<main class="w-full">
|
||||
<div class="relative pt-32 pb-10 max-w-5xl mx-auto px-6" >
|
||||
<div class="container flex flex-col gap-6 md:flex-row" >
|
||||
<div class="flex min-w-[209px] flex-col gap-2" >
|
||||
<span class="text-xs font-medium text-gray-800 dark:text-gray-200 uppercase" >
|
||||
General
|
||||
</span>
|
||||
<div class="flex divide-x flex-col overflow-hidden" >
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm font-medium no-underline rounded border-none bg-gray-200 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M14.06 9.94L12 9l2.06-.94L15 6l.94 2.06L18 9l-2.06.94L15 12zM4 14l.94-2.06L7 11l-2.06-.94L4 8l-.94 2.06L1 11l2.06.94zm4.5-5l1.09-2.41L12 5.5L9.59 4.41L8.5 2L7.41 4.41L5 5.5l2.41 1.09zm-4 11.5l6-6.01l4 4L23 8.93l-1.41-1.41l-7.09 7.97l-4-4L3 19z" /></svg>
|
||||
Performance
|
||||
</div>
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm no-underline rounded border-none text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 7a4 4 0 1 0 8 0a4 4 0 1 0-8 0M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2m1-17.87a4 4 0 0 1 0 7.75M21 21v-2a4 4 0 0 0-3-3.85" /></svg>
|
||||
Users
|
||||
</div>
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm no-underline rounded border-none text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3a12 12 0 0 0 8.5 3A12 12 0 0 1 12 21A12 12 0 0 1 3.5 6A12 12 0 0 0 12 3" /></svg>
|
||||
Security
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-gray-800 dark:text-gray-200 uppercase mt-6" >
|
||||
Network
|
||||
</span>
|
||||
<div class="flex divide-x flex-col overflow-hidden" >
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm no-underline rounded border-none text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M20.5 9.035a9.004 9.004 0 0 0-17 0m17 0c.324.928.5 1.926.5 2.965a9 9 0 0 1-.5 2.966m0-5.931h-17m0 0A9 9 0 0 0 3 12a9 9 0 0 0 .5 2.966m0 0a9.004 9.004 0 0 0 17 0m-17 0h17" /><path d="M12 21c4.97-4.97 4.97-13.03 0-18c-4.97 4.97-4.97 13.03 0 18" /></g></svg>
|
||||
API requests
|
||||
</div>
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm no-underline rounded border-none text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" /><path fill="currentColor" d="M8.207 11.757a1 1 0 0 1 0 1.415L6.38 15H16a1 1 0 1 1 0 2H6.38l1.828 1.828a1 1 0 1 1-1.414 1.415l-3.536-3.536a1 1 0 0 1 0-1.414l3.536-3.536a1 1 0 0 1 1.414 0Zm7.586-8a1 1 0 0 1 1.32-.083l.094.083l3.536 3.536a1 1 0 0 1 .083 1.32l-.083.094l-3.536 3.535a1 1 0 0 1-1.497-1.32l.083-.094L17.62 9H8a1 1 0 0 1-.117-1.993L8 7h9.621l-1.828-1.83a1 1 0 0 1 0-1.414Z" /></g></svg>
|
||||
Data transfer
|
||||
</div>
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm no-underline rounded border-none text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" d="M2.5 5.724V8c0 .248.238.7 1.169 1.159c.874.43 2.144.745 3.62.822a.75.75 0 1 1-.078 1.498c-1.622-.085-3.102-.432-4.204-.975a6 6 0 0 1-.507-.28V12.5c0 .133.058.318.282.551c.227.237.591.483 1.101.707c1.015.447 2.47.742 4.117.742q.61 0 1.183-.052a.751.751 0 1 1 .134 1.494Q8.676 15.999 8 16c-1.805 0-3.475-.32-4.721-.869c-.623-.274-1.173-.619-1.579-1.041c-.408-.425-.7-.964-.7-1.59v-9c0-.626.292-1.165.7-1.591c.406-.42.956-.766 1.579-1.04C4.525.32 6.195 0 8 0s3.476.32 4.721.869c.623.274 1.173.619 1.579 1.041c.408.425.7.964.7 1.59s-.292 1.165-.7 1.591c-.406.42-.956.766-1.578 1.04C11.475 6.68 9.805 7 8 7s-3.475-.32-4.721-.869a6 6 0 0 1-.779-.407m0-2.224c0 .133.058.318.282.551c.227.237.591.483 1.101.707C4.898 5.205 6.353 5.5 8 5.5s3.101-.295 4.118-.742c.508-.224.873-.471 1.1-.708c.224-.232.282-.417.282-.55s-.058-.318-.282-.551c-.227-.237-.591-.483-1.101-.707C11.102 1.795 9.647 1.5 8 1.5s-3.101.295-4.118.742c-.508.224-.873.471-1.1.708c-.224.232-.282.417-.282.55" /><path fill="currentColor" d="M14.49 7.582a.375.375 0 0 0-.66-.313l-3.625 4.625a.375.375 0 0 0 .295.606h2.127l-.619 2.922a.375.375 0 0 0 .666.304l3.125-4.125A.375.375 0 0 0 15.5 11h-1.778z" /></svg>
|
||||
WAN cache
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-gray-800 dark:text-gray-200 uppercase mt-6" >
|
||||
Admin
|
||||
</span>
|
||||
<div class="flex divide-x flex-col overflow-hidden" >
|
||||
<div class="[&>svg]:size-5 flex flex-row gap-2 items-center px-3 py-2.5 text-sm no-underline rounded border-none text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3" /><path d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z" /></g></svg>
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" >
|
||||
<div class="flex flex-col gap-y-4" >
|
||||
<div class="w-full bg-white dark:bg-black ring-gray-200 ring-2 dark:ring-gray-800 rounded-[10px]">
|
||||
<div class="size-full ring-1 ring-gray-200 dark:ring-gray-800 rounded-md flex justify-center items-center h-[300px] gap-2 flex-col">
|
||||
<div class="border-2 border-gray-100 dark:border-gray-900 size-[60px] text-gray-600 dark:text-gray-400 rounded-lg justify-center flex items-center [&>svg]:size-8">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="m665.216 768l110.848 192h-73.856L591.36 768H433.024L322.176 960H248.32l110.848-192H160a32 32 0 0 1-32-32V192H64a32 32 0 0 1 0-64h896a32 32 0 1 1 0 64h-64v544a32 32 0 0 1-32 32zM832 192H192v512h640zM352 448a32 32 0 0 1 32 32v64a32 32 0 0 1-64 0v-64a32 32 0 0 1 32-32m160-64a32 32 0 0 1 32 32v128a32 32 0 0 1-64 0V416a32 32 0 0 1 32-32m160-64a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V352a32 32 0 0 1 32-32" /></svg>
|
||||
</div>
|
||||
<div class="max-w-[340px] w-full text-black dark:text-white text-lg text-center font-semibold font-title flex gap-1 justify-center h-max items-center">
|
||||
No data yet
|
||||
<div class="select-none text-[#ff990a] bg-[#ff990a]/30 h-max uppercase overflow-hidden rounded-md px-2 py-1 text-xs transition-colors duration-200 ease-out font-semibold font-title">
|
||||
<span>Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300 max-w-[340px] text-center" >
|
||||
Once you have installed a game and started playing data should start flowing into Nestri
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<div class="bg-white p-8 dark:bg-black ring-gray-200 ring-2 dark:ring-gray-800 relative w-full rounded-[10px] h-[240px] min-h-[240px]">
|
||||
<div class="relative w-full h-full flex flex-col gap-2">
|
||||
<p class="font-medium font-title" >GPU usage</p>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-8 dark:bg-black ring-gray-200 ring-2 dark:ring-gray-800 relative w-full rounded-[10px] h-[240px] min-h-[240px]">
|
||||
<div class="relative w-full h-full flex flex-col gap-2">
|
||||
<p class="font-medium font-title" >CPU usage</p>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
})
|
||||
@@ -1,490 +0,0 @@
|
||||
import { cn } from "@nestri/ui/design";
|
||||
import type Nestri from "@nestri/sdk";
|
||||
import { Modal } from "@qwik-ui/headless";
|
||||
import { Avatar, Icons } from "@nestri/ui";
|
||||
import { Link, routeLoader$ } from "@builder.io/qwik-city";
|
||||
import { HomeNavBar, SimpleFooter, GameStoreButton } from "@nestri/ui";
|
||||
import { component$, useSignal, useStore, useVisibleTask$ } from "@builder.io/qwik";
|
||||
|
||||
const games = [
|
||||
{
|
||||
id: 2507950,
|
||||
name: "Delta Force",
|
||||
image: "https://assets-prd.ignimgs.com/2024/08/28/delta-force-button-replacement-1724855313566.jpg"
|
||||
},
|
||||
{
|
||||
id: 870780,
|
||||
name: "Control Ultimate Edition",
|
||||
image: "https://assets-prd.ignimgs.com/2023/04/08/sq-nswitchds-controlultimateeditioncloudversion-image500w-1680973421643.jpg"
|
||||
},
|
||||
{
|
||||
id: 1172470,
|
||||
name: "Apex Legends",
|
||||
image: "https://assets-prd.ignimgs.com/2023/02/16/apexrevelry-1676588335122.jpg"
|
||||
},
|
||||
{
|
||||
id: 914800,
|
||||
name: "Coffee Talk",
|
||||
image: "https://assets-prd.ignimgs.com/2022/11/09/coffee-talk-episode-1-button-fin-1668033710468.jpg"
|
||||
}, {
|
||||
id: 1085220,
|
||||
name: "Figment 2: Creed Valley",
|
||||
image: "https://assets-prd.ignimgs.com/2021/12/15/figment-2-button-1639602944843.jpg"
|
||||
}, {
|
||||
id: 1568400,
|
||||
name: "Sheepy: A Short Adventure",
|
||||
image: "https://assets-prd.ignimgs.com/2024/04/08/sheepy-1712557253260.jpg"
|
||||
}, {
|
||||
id: 271590,
|
||||
name: "Grand Theft Auto V",
|
||||
image: "https://assets-prd.ignimgs.com/2021/12/17/gta-5-button-2021-1639777058682.jpg"
|
||||
}, {
|
||||
id: 1086940,
|
||||
name: "Baldur's Gate 3",
|
||||
image: "https://assets-prd.ignimgs.com/2023/08/24/baldursg3-1692894717196.jpeg"
|
||||
}, {
|
||||
id: 1091500,
|
||||
name: "Cyberpunk 2077",
|
||||
image: "https://assets-prd.ignimgs.com/2020/07/16/cyberpunk-2077-button-fin-1594877291453.jpg"
|
||||
}, {
|
||||
id: 221100,
|
||||
name: "DayZ",
|
||||
image: "https://assets-prd.ignimgs.com/2021/12/20/dayz-1640044421966.jpg"
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
export const useCurrentProfile = routeLoader$(async ({ sharedMap }) => {
|
||||
const res = sharedMap.get("profile") as Nestri.Users.UserRetrieveResponse.Data | null
|
||||
|
||||
return res
|
||||
// return {
|
||||
// avatarUrl: undefined,
|
||||
// discriminator: 47,
|
||||
// username: "WanjohiRyan"
|
||||
// }
|
||||
})
|
||||
//bg-blue-100 rounded-lg p-4 min-w-16 text-center
|
||||
const TimeUnit = ({ value, label }: { value: number, label: string }) => (
|
||||
<div class="flex flex-col items-center bg-blue-100 rounded-lg p-4">
|
||||
<div style={{ "--line-height": "2rem" }} class="flex leading-none items-center text-[2rem] font-medium font-title">
|
||||
{new Array(2).fill(0).map((_, key) => {
|
||||
const [digitOne, digitTwo] = value.toString().padStart(2, '0')
|
||||
return (
|
||||
<div style={{ "--digit-one": Number(digitOne), "--digit-two": Number(digitTwo) }} key={`digit-${key}`} class={cn("h-[--line-height] overflow-hidden", key == 0 ? "first-of-type:[--v:var(--digit-one)]" : "last-of-type:[--v:var(--digit-two)]")} >
|
||||
<div class={cn("digit_timing flex flex-col", key == 0 ? "items-end" : "items-start")}>
|
||||
<div>9</div>
|
||||
<div>0</div>
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
<div>3</div>
|
||||
<div>4</div>
|
||||
<div>5</div>
|
||||
<div>6</div>
|
||||
<div>7</div>
|
||||
<div>8</div>
|
||||
<div>9</div>
|
||||
<div>0</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div class="text-xs mt-1 text-gray-600">{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const random = Math.floor(100 * Math.random())
|
||||
|
||||
export default component$(() => {
|
||||
const profile = useCurrentProfile()
|
||||
const isNewPerson = useSignal(false)
|
||||
const targetDate = new Date('2025-01-29T23:59:00Z');
|
||||
|
||||
const timeLeft = useStore({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0
|
||||
})
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(() => {
|
||||
isNewPerson.value = true
|
||||
|
||||
const calculateTimeLeft = () => {
|
||||
const difference = targetDate.getTime() - new Date().getTime();
|
||||
|
||||
if (difference > 0) {
|
||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((difference % (1000 * 60)) / 1000);
|
||||
|
||||
timeLeft.days = days
|
||||
timeLeft.hours = hours
|
||||
timeLeft.minutes = minutes
|
||||
timeLeft.seconds = seconds
|
||||
}
|
||||
};
|
||||
|
||||
calculateTimeLeft();
|
||||
|
||||
const timer = setInterval(calculateTimeLeft, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
})
|
||||
|
||||
return (
|
||||
<main class="flex w-screen h-full flex-col relative">
|
||||
{profile.value && <HomeNavBar avatarUrl={profile.value.avatarUrl} discriminator={profile.value.discriminator} username={profile.value.username} />}
|
||||
<section class="max-w-[750px] w-full mx-auto flex flex-col gap-3 px-5 pt-20 pb-14 ">
|
||||
<div class="flex flex-col gap-6 w-full py-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<GameStoreButton />
|
||||
<Modal.Root class="w-full">
|
||||
<Modal.Trigger class="border-gray-400/70 w-full dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] group transition-all border-dashed duration-200 border-[2px] h-14 rounded-xl pl-4 gap-2 flex items-center justify-between overflow-hidden hover:bg-gray-300/70 dark:hover:bg-gray-700/70 outline-none disabled:opacity-50">
|
||||
<span class="py-2 text-gray-600/70 dark:text-gray-400/70 leading-none group-hover:text-black dark:group-hover:text-white shrink truncate flex text-start justify-center items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.505 2h-1.501c-3.281 0-4.921 0-6.084.814a4.5 4.5 0 0 0-1.106 1.105C2 5.08 2 6.72 2 10s0 4.919.814 6.081a4.5 4.5 0 0 0 1.106 1.105C5.083 18 6.723 18 10.004 18h4.002c3.28 0 4.921 0 6.084-.814a4.5 4.5 0 0 0 1.105-1.105c.63-.897.772-2.08.805-4.081m-8-6h4m0 0h4m-4 0V2m0 4v4m-7 5h2m-1 3v4m-4 0h8" color="currentColor" /></svg>
|
||||
Add another Linux machine
|
||||
</span>
|
||||
</Modal.Trigger>
|
||||
<Modal.Panel class="
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-[340px] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
|
||||
dark:[box-shadow: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] dark:bg-[#222b]
|
||||
[box-shadow: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] bg-[#fffd]
|
||||
backdrop-blur-lg py-4 px-5 modal" >
|
||||
<div class="size-full flex flex-col">
|
||||
<div class="flex justify-between items-start ">
|
||||
<div class="mb-3 size-14 rounded-full text-[#939597] dark:text-[#d2d4d7] bg-[rgba(19,21,23,0.04)] dark:bg-white/[.08] flex items-center justify-center [&>svg]:size-8" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.505 2h-1.501c-3.281 0-4.921 0-6.084.814a4.5 4.5 0 0 0-1.106 1.105C2 5.08 2 6.72 2 10s0 4.919.814 6.081a4.5 4.5 0 0 0 1.106 1.105C5.083 18 6.723 18 10.004 18h4.002c3.28 0 4.921 0 6.084-.814a4.5 4.5 0 0 0 1.105-1.105c.63-.897.772-2.08.805-4.081m-8-6h4m0 0h4m-4 0V2m0 4v4m-7 5h2m-1 3v4m-4 0h8" color="currentColor" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dark:text-white text-black">
|
||||
<h3 class="font-semibold text-2xl tracking-tight mb-2 font-title">Add a Linux machine</h3>
|
||||
<div class="text-sm dark:text-white/[.79] text-[rgba(19,21,23,0.64)]" >
|
||||
Download and install Nestri on your remote server or computer to connect it. Then paste the generated machine id here.
|
||||
</div>
|
||||
</div>
|
||||
<form action="#" class="mt-3 flex flex-col gap-3" >
|
||||
<input placeholder="fc27f428f9ca47d4b41b707ae0c62090" class="transition-all duration-200 w-full px-2 py-3 h-10 border text-black dark:text-white dark:border-[#343434] border-[#e2e2e2] rounded-md text-sm outline-none bg-white dark:bg-[rgba(19,21,23,0.64)] leading-none [background-image:-webkit-linear-gradient(hsla(0,0%,100%,0),hsla(0,0%,100%,0))]
|
||||
focus:[box-shadow:0_0_0_2px_#fcfcfc,0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070]" />
|
||||
<button class="w-full h-[calc(2.25rem+2*1px)] transition-all duration-200 hover:[box-shadow:0_0_0_2px_#fcfcfc,0_0_0_4px_#8f8f8f] dark:hover:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] focus:[box-shadow:0_0_0_2px_#fcfcfc,0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] outline-none bg-primary-500 text-white rounded-lg text-sm" >Add machine</button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
</div>
|
||||
</div >
|
||||
<div class="gap-2 w-full flex-col flex">
|
||||
<hr class="border-none h-[1.5px] dark:bg-gray-700 bg-gray-300 w-full" />
|
||||
<div class="flex flex-col justify-center py-2 px-3 items-start w-full ">
|
||||
<div class="text-gray-600/70 dark:text-gray-400/70 text-sm leading-none flex justify-between items-center w-full py-1">
|
||||
<span class="text-xl text-gray-700 dark:text-gray-300 leading-none font-bold font-title flex gap-2 items-center pb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 20 20"><path fill="currentColor" d="M2.049 9.112a8.001 8.001 0 1 1 9.718 8.692a1.5 1.5 0 0 0-.206-1.865l-.01-.01q.244-.355.47-.837a9.3 9.3 0 0 0 .56-1.592H9.744q.17-.478.229-1h2.82A15 15 0 0 0 13 10c0-.883-.073-1.725-.206-2.5H7.206l-.05.315a4.5 4.5 0 0 0-.971-.263l.008-.052H3.46q-.112.291-.198.595c-.462.265-.873.61-1.213 1.017m9.973-4.204C11.407 3.59 10.657 3 10 3s-1.407.59-2.022 1.908A9.3 9.3 0 0 0 7.42 6.5h5.162a9.3 9.3 0 0 0-.56-1.592M6.389 6.5c.176-.743.407-1.422.683-2.015c.186-.399.401-.773.642-1.103A7.02 7.02 0 0 0 3.936 6.5zm9.675 7H13.61a10.5 10.5 0 0 1-.683 2.015a6.6 6.6 0 0 1-.642 1.103a7.02 7.02 0 0 0 3.778-3.118m-2.257-1h2.733c.297-.776.46-1.62.46-2.5s-.163-1.724-.46-2.5h-2.733c.126.788.193 1.63.193 2.5s-.067 1.712-.193 2.5m2.257-6a7.02 7.02 0 0 0-3.778-3.118c.241.33.456.704.642 1.103c.276.593.507 1.272.683 2.015zm-7.76 7.596a3.5 3.5 0 1 0-.707.707l2.55 2.55a.5.5 0 0 0 .707-.707zM8 12a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0" /></svg>
|
||||
Find people to play with
|
||||
</span>
|
||||
</div>
|
||||
<ul class="list-none ml-4 relative w-[calc(100%-1rem)]">
|
||||
{games.slice(5, 8).sort().map((game, key) => (
|
||||
<Modal.Root key={`find-${key}`} >
|
||||
<Modal.Trigger class="gap-3.5 text-left hover:bg-gray-300/70 dark:hover:bg-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none group rounded-lg px-3 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] flex items-center w-full">
|
||||
<img height={52} width={52} draggable={false} class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] group-hover:scale-105 group-hover:shadow-lg group-hover:shadow-gray-900 select-none rounded-lg aspect-square w-[80px]" src={game.image} alt={game.name} />
|
||||
<div class={cn("w-full h-[100px] overflow-hidden border-b-2 border-gray-400/70 dark:border-gray-700/70 flex flex-col gap-2 justify-center", key == 2 && "border-none")}>
|
||||
<span class="font-medium tracking-tighter text-gray-700 dark:text-gray-300 max-w-full text-lg font-title truncate leading-none">
|
||||
{game.name}
|
||||
</span>
|
||||
<div class="flex items-center px-2 gap-2 w-full">
|
||||
<div
|
||||
class="items-center flex"
|
||||
style={{
|
||||
"--size": "1.25rem",
|
||||
"--cutout-avatar-percentage-visible": 0.4,
|
||||
"--head-margin-percentage": 0.2
|
||||
}}>
|
||||
{new Array(3).fill(0).map((_, key) => (
|
||||
<div key={key} class="relative items-start flex ml-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))]">
|
||||
<div
|
||||
class="[&>svg]:size-5"
|
||||
style={{
|
||||
maskSize: "100% 100%",
|
||||
maskRepeat: "no-repeat",
|
||||
maskPosition: "center",
|
||||
maskComposite: "subtract",
|
||||
maskImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><circle r="0.5" cx="0.5" cy="0.5"/></svg>'),url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><circle r="0.6" cx="1.1" cy="0.5"/></svg>')`
|
||||
}}
|
||||
>
|
||||
<Avatar name={((key + 1) * random).toString()} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div class="[&>svg]:size-[--size] ml-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))] relative flex items-center justify-center">
|
||||
<Avatar name={((key + 1) * random).toString()} />
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-normal text-gray-600 dark:text-gray-400 text-sm w-full truncate">{`${(key + 1) * random} people are currently playing this game`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Trigger>
|
||||
<Modal.Panel class="modal-sheet [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] rounded-xl border dark:border-[#343434] border-[#e2e2e2] right-2 top-0 mr-2 mt-2
|
||||
dark:[box-shadow: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] dark:bg-[#222b]
|
||||
[box-shadow: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] bg-[#fffd]
|
||||
backdrop-blur-lg">
|
||||
<div class=" min-h-[calc(100dvh-1rem)] h-[calc(100dvh-1rem)] w-[550px] relative " >
|
||||
<div class="sticky top-0 w-full z-10 backdrop-blur-lg dark:bg-[rgba(19,21,23,0.48)] dark:border-white/[.08] border-b py-2 px-3 min-h-12 gap-3 flex justify-between items-center" >
|
||||
<Modal.Close class="dark:text-white/[.64] text-[rgba(19,21,23,0.64)] [&>svg]:size-5 [&>svg]:scale-[1.2] hover:text-white dark:hover:text-[rgb(19,21,23)] py-1.5 px-2.5 rounded-lg transition-all duration-200 hover:bg-[rgba(19,21,23,0.64)] dark:hover:bg-white/[.64]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="M24 0v24H0V0zM12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" /><path fill="currentColor" d="M6.293 6.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414-1.414L10.586 12L6.293 7.707a1 1 0 0 1 0-1.414m6 0a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414-1.414L16.586 12l-4.293-4.293a1 1 0 0 1 0-1.414" /></g></svg>
|
||||
</Modal.Close>
|
||||
<div class="gap-2 flex justify-between flex-1 items-center ">
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<button class="dark:text-white/[.64] dark:bg-white/[.08] text-[rgba(19,21,23,0.64)] bg-[rgba(19,21,23,0.04)] font-medium py-1.5 px-2.5 rounded-lg flex items-center gap-1 transition-all duration-200 [&>svg]:size-5 text-sm hover:text-white dark:hover:text-[rgb(19,21,23)] hover:bg-[rgba(19,21,23,0.64)] dark:hover:bg-white/[.64]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2"><path d="M14 7c0-.932 0-1.398-.152-1.765a2 2 0 0 0-1.083-1.083C12.398 4 11.932 4 11 4H8c-1.886 0-2.828 0-3.414.586S4 6.114 4 8v3c0 .932 0 1.398.152 1.765a2 2 0 0 0 1.083 1.083C5.602 14 6.068 14 7 14" /><rect width="10" height="10" x="10" y="10" rx="2" /></g></svg>
|
||||
Copy link
|
||||
</button>
|
||||
<button class="dark:text-white/[.64] dark:bg-white/[.08] text-[rgba(19,21,23,0.64)] bg-[rgba(19,21,23,0.04)] font-medium py-1.5 px-2.5 rounded-lg flex items-center gap-1 transition-all duration-200 [&>svg]:size-5 text-sm hover:text-white dark:hover:text-[rgb(19,21,23)] hover:bg-[rgba(19,21,23,0.64)] dark:hover:bg-white/[.64]">
|
||||
Game page
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 18L18 6m0 0H9m9 0v9" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 pt-10 gap-6 flex flex-col text-white" >
|
||||
<div class="m-4 mb-2 relative flex items-center justify-center" >
|
||||
<img src={game.image} height={280} width={280} class="rounded-xl bg-white/[.08] aspect-square size-[280px]" />
|
||||
</div>
|
||||
<div class="flex gap-2 flex-col dark:text-white text-black" >
|
||||
<h1 class="text-3xl font-title font-bold tracking-tight leading-none" >{game.name}</h1>
|
||||
<p class="dark:text-gray-400 text-gray-600 [display:-webkit-box] max-w-full overflow-hidden [-webkit-line-clamp:3] [-webkit-box-orient:vertical]" >
|
||||
A short handcrafted pixel art platformer that follows Sheepy, an abandoned plushy brought to life. Sheepy: A Short Adventure is the first short game from MrSuicideSheep.
|
||||
</p>
|
||||
<div class="gap-y-1 gap-x-2 flex-wrap flex " >
|
||||
<button class="[&>svg]:size-[14px] cursor-pointer hover:border-primary-500 hover:text-primary-500 border-2 border-[rgba(19,21,23,0.08)] dark:border-white/[.16] items-center inline-flex py-1 px-2 rounded-[100px] gap-0.5 text-[rgba(19,21,23,0.36)] dark:text-[hsla(0,0%,100%,.5)] text-[0.875rem] font-medium transition-all duration-200" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Adventure
|
||||
</button>
|
||||
<button class="[&>svg]:size-[14px] cursor-pointer hover:border-primary-500 hover:text-primary-500 border-2 border-[rgba(19,21,23,0.08)] dark:border-white/[.16] items-center inline-flex py-1 px-2 rounded-[100px] gap-0.5 text-[rgba(19,21,23,0.36)] dark:text-[hsla(0,0%,100%,.5)] text-[0.875rem] font-medium transition-all duration-200" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Free to play
|
||||
</button>
|
||||
<button class="[&>svg]:size-[14px] cursor-pointer hover:border-primary-500 hover:text-primary-500 border-2 border-[rgba(19,21,23,0.08)] dark:border-white/[.16] items-center inline-flex py-1 px-2 rounded-[100px] gap-0.5 text-[rgba(19,21,23,0.36)] dark:text-[hsla(0,0%,100%,.5)] text-[0.875rem] font-medium transition-all duration-200" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Indie
|
||||
</button>
|
||||
</div>
|
||||
<div class="py-3 px-4 overflow-hidden bg-white/[.08] dark:bg-white/[.04] border dark:border-[#343434] border-[#e2e2e2] rounded-xl [box-shadow:0_1px_4px_rgba(0,0,0,.1)] dark:[box-shadow:0_1px_4px_rgba(0,0,0,.15)]" >
|
||||
<div class="dark:bg-white/[.08] bg-[rgba(19,21,23,0.04)] mx-[calc(-1rem+1px)] my-[calc(-0.75rem+1px)] mb-3 py-[calc(0.5rem-1px)] px-[calc(1rem-1px)]" >
|
||||
<p class="text-sm text-[rgba(19,21,23,0.64)] dark:text-[hsla(0,0%,100%,.79)] font-medium font-title" >Join a Nestri party</p>
|
||||
</div>
|
||||
<div class="gap-3 flex flex-col">
|
||||
<div class=" border-b border-[rgba(19,21,23,0.08)] dark:border-white/[.08] gap-3 flex flex-col [margin:-0.5rem_-1rem_0.25rem] [padding:0.5rem_1rem_0.75rem] ">
|
||||
<div class="flex gap-3">
|
||||
<div class="size-7 mt-2 shrink-0 flex items-center justify-center" >
|
||||
<img alt="ESRN-Teen" width={40} height={40} src="https://oyster.ignimgs.com/mediawiki/apis.ign.com/ratings/b/bf/ESRB-ver2013_T.png?width=325" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium font-title" >Teen [13+]</p>
|
||||
<span class="mt-[1px] text-sm leading-none text-[rgba(19,21,23,0.64)] dark:text-[hsla(0,0%,100%,.79)]" >Mild Language, Violence, Blood and Gore, Drug References</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" mb-1 dark:text-white w-full leading-none -my-1 py-1">
|
||||
<button class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_#fcfcfc,0_0_0_4px_#8f8f8f] dark:hover:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] focus:[box-shadow:0_0_0_2px_#fcfcfc,0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] font-medium font-title rounded-lg flex h-[calc(2.25rem+2*1px)] flex-col text-white w-full leading-none truncate bg-primary-500 items-center justify-center" >
|
||||
Join this party
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
))}
|
||||
<div class="[border:1px_dashed_theme(colors.gray.300)] dark:[border:1px_dashed_theme(colors.gray.800)] [mask-image:linear-gradient(rgb(0,0,0)_0%,_rgb(0,0,0)_calc(100%-120px),_transparent_100%)] bottom-0 top-0 -left-[0.4625rem] absolute" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2 w-full flex-col flex">
|
||||
<hr class="border-none h-[1.5px] dark:bg-gray-700 bg-gray-300 w-full" />
|
||||
<div class="text-gray-600/70 dark:text-gray-400/70 text-sm leading-none flex justify-between py-2 px-3 items-end">
|
||||
<span class="text-xl text-gray-700 dark:text-gray-300 leading-none font-bold font-title flex gap-2 ">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 22c-.818 0-1.6-.33-3.163-.99C4.946 19.366 3 18.543 3 17.16V7m9 15c.818 0 1.6-.33 3.163-.99C19.054 19.366 21 18.543 21 17.16V7m-9 15V11.355M8.326 9.691L5.405 8.278C3.802 7.502 3 7.114 3 6.5s.802-1.002 2.405-1.778l2.92-1.413C10.13 2.436 11.03 2 12 2s1.871.436 3.674 1.309l2.921 1.413C20.198 5.498 21 5.886 21 6.5s-.802 1.002-2.405 1.778l-2.92 1.413C13.87 10.564 12.97 11 12 11s-1.871-.436-3.674-1.309M6 12l2 1m9-9L7 9" color="currentColor" /></svg>
|
||||
Your Games
|
||||
</span>
|
||||
<button class="flex gap-1 items-center cursor-pointer hover:text-gray-800 dark:hover:text-gray-200 transition-all duration-200 outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 256 256"><path fill="currentColor" d="M248 128a87.34 87.34 0 0 1-17.6 52.81a8 8 0 1 1-12.8-9.62A71.34 71.34 0 0 0 232 128a72 72 0 0 0-144 0a8 8 0 0 1-16 0a88 88 0 0 1 3.29-23.88C74.2 104 73.1 104 72 104a48 48 0 0 0 0 96h24a8 8 0 0 1 0 16H72a64 64 0 1 1 9.29-127.32A88 88 0 0 1 248 128m-69.66 42.34L160 188.69V128a8 8 0 0 0-16 0v60.69l-18.34-18.35a8 8 0 0 0-11.32 11.32l32 32a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32" /></svg>
|
||||
<span>Install a game</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="relative py-3 w-full grid sm:grid-cols-3 grid-cols-2 list-none after:pointer-events-none after:select-none after:w-full after:h-[120px] after:fixed after:z-10 after:backdrop-blur-[1px] after:bg-gradient-to-b after:from-transparent after:to-gray-100 dark:after:to-gray-900 after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.100)_25%,transparent)] dark:after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.900)_25%,transparent)] after:left-0 after:-bottom-[1px]">
|
||||
{games.map((game, key) => (
|
||||
<Modal.Root key={`game-${key}`} >
|
||||
<Modal.Trigger class="hover:bg-gray-300/70 dark:hover:bg-gray-700/70 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] px-2 py-2 rounded-[15px] hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none size-full group [&_*]:transition-all [&_*]:duration-150 flex flex-col gap-2" key={key}>
|
||||
<img draggable={false} alt={game.name} class="select-none [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] group-hover:scale-[1.01] group-hover:shadow-lg group-hover:shadow-gray-900 w-full rounded-xl aspect-square" src={game.image} height={90} width={90} />
|
||||
<div class="flex flex-col px-2 w-full">
|
||||
<span class="max-w-full truncate">{game.name}</span>
|
||||
</div>
|
||||
</Modal.Trigger>
|
||||
<Modal.Panel class="dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] rounded-xl border dark:border-[#343434] border-gray-600/70
|
||||
dark:[box-shadow: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] dark:bg-[rgb(22,22,22)]
|
||||
box-shadow: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] bg-gray-300
|
||||
backdrop-blur-lg modal" >
|
||||
<div class="flex flex-col min-w-[17rem] relative text-black/70 dark:text-white/70 w-full max-w-[41.8125rem] min-h-[min(90%,100%-3rem)]" >
|
||||
<div class="flex-1 relative w-full " >
|
||||
<div class="relative w-full pb-[56.25%] overflow-hidden after:z-[2] after:absolute after:inset-0 dark:after:[background:linear-gradient(40deg,rgb(22,22,22)_24.16%,rgba(6,10,23,0)_56.61%),linear-gradient(0deg,rgb(22,22,22)_3.91%,rgba(6,10,23,0)_69.26%)] after:[background:linear-gradient(0deg,theme(colors.gray.300)_3.91%,theme(colors.gray.300/0.03)_69.26%)]" >
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${game.id}/library_hero.jpg)`
|
||||
}}
|
||||
class={cn("absolute inset-0 z-[1] [transition:opacity_300ms_ease-in-out] bg-cover bg-[center_top] bg-no-repeat")} />
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(${game.image})`
|
||||
}}
|
||||
class={cn("absolute inset-0 -z-[1] bg-cover bg-[center_top] bg-no-repeat blur-[4rem]")} />
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${game.id}/logo.png)`
|
||||
}}
|
||||
class="absolute dark:bottom-0 min-[600px]:left-10 left-4 bg-contain bg-[center_bottom] bg-no-repeat min-[600px]:max-w-[40%] w-full aspect-video z-[3] bottom-[20px] " />
|
||||
</div>
|
||||
<div class="min-[600px]:p-10 min-[600px]:pt-4 pt-4 px-4 pb-6" >
|
||||
<ul class="[&_svg]:-mt-[2px] xl:mb-3 min-[960px]:mb-2 min-[600px]:leading-5 mb-4 leading-[0.625rem] list-none flex w-full" >
|
||||
<li class="mr-2 flex gap-0.5 [&>svg]:size-[14px] items-center justify-center text-sm dark:bg-[rgb(65,65,65)] bg-[rgb(171,171,171)] px-2 py-1 w-max rounded-md" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Shooter
|
||||
</li>
|
||||
<li class="mr-2 flex gap-0.5 [&>svg]:size-[14px] items-center justify-center text-sm dark:bg-[rgb(65,65,65)] bg-[rgb(171,171,171)] px-2 py-1 w-max rounded-md" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Action
|
||||
</li>
|
||||
<li class="mr-2 flex gap-0.5 [&>svg]:size-[14px] items-center justify-center text-sm dark:bg-[rgb(65,65,65)] bg-[rgb(171,171,171)] px-2 py-1 w-max rounded-md" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Free to play
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-black/90 dark:text-white/90 tracking-tight" >
|
||||
Delta Force is a first-person shooter which offers players both a single player campaign based on the movie Black Hawk Down, but also large-scale PvP multiplayer action. The game was formerly known as Delta Force: Hawk Ops.
|
||||
</p>
|
||||
<div class="sm:pt-10 sm:block hidden" >
|
||||
<button class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:hover:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] focus:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] font-medium font-title rounded-lg flex h-[calc(2.25rem+2*1px)] flex-col text-white w-full leading-none truncate bg-primary-500 items-center justify-center" >
|
||||
Play Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section >
|
||||
<SimpleFooter />
|
||||
<Modal.Root bind:show={isNewPerson} closeOnBackdropClick={false}>
|
||||
<Modal.Panel
|
||||
class="dark:bg-gray-700 bg-white [box-shadow:0_8px_30px_rgba(0,0,0,.12)]
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] rounded-xl
|
||||
backdrop-blur-md modal outline-none overflow-visible">
|
||||
<div class="relative select-none w-full max-w-[740px] max-h-[75vh] h-full">
|
||||
<div class="pointer-events-auto flex w-full flex-col rounded-xl relative bg-white dark:bg-gray-700/70">
|
||||
<span class="absolute inset-0 z-0 size-full rounded-xl bg-gradient-to-b from-[rgb(17,168,255)] to-[rgb(160,221,255)] dark:from-[rgb(93,94,162)] dark:via-[rgb(33,143,205)] dark:to-[rgb(112,203,255)]" />
|
||||
<div class="rounded-xl relative p-6">
|
||||
<div class="flex w-full flex-row justify-start gap-6">
|
||||
<div class="-mt-6 flex w-[280px] flex-col items-center gap-6 rounded-b-xl bg-gradient-to-b dark:from-gray-100 dark:to-gray-50 from-gray-200 to-gray-100 px-6 pb-6 shadow-lg">
|
||||
<div class="-mt-7 flex flex-row items-center justify-center gap-2 text-center text-sm text-white/80">
|
||||
<svg width="96" height="52" viewBox="0 0 96 52" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#filter0_di_4064_903)">
|
||||
<mask id="path-1-inside-1_4064_903" fill="white"><path fill-rule="evenodd" clip-rule="evenodd" d="M64 20C64 22.0476 65.4906 24 67.5382 24H81.6C83.8402 24 84.9603 24 85.816 24.436C86.5686 24.8195 87.1805 25.4314 87.564 26.184C88 27.0397 88 28.1598 88 30.4V33.6C88 35.8402 88 36.9603 87.564 37.816C87.1805 38.5686 86.5686 39.1805 85.816 39.564C84.9603 40 83.8402 40 81.6 40H14.4C12.1598 40 11.0397 40 10.184 39.564C9.43139 39.1805 8.81947 38.5686 8.43597 37.816C8 36.9603 8 35.8402 8 33.6V30.4C8 28.1598 8 27.0397 8.43597 26.184C8.81947 25.4314 9.43139 24.8195 10.184 24.436C11.0397 24 12.1598 24 14.4 24H28.4617C30.5094 24 32 22.0476 32 20C32 11.1634 39.1634 4 48 4C56.8366 4 64 11.1634 64 20ZM48 24C50.2091 24 52 22.2091 52 20C52 17.7909 50.2091 16 48 16C45.7909 16 44 17.7909 44 20C44 22.2091 45.7909 24 48 24Z"></path></mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64 20C64 22.0476 65.4906 24 67.5382 24H81.6C83.8402 24 84.9603 24 85.816 24.436C86.5686 24.8195 87.1805 25.4314 87.564 26.184C88 27.0397 88 28.1598 88 30.4V33.6C88 35.8402 88 36.9603 87.564 37.816C87.1805 38.5686 86.5686 39.1805 85.816 39.564C84.9603 40 83.8402 40 81.6 40H14.4C12.1598 40 11.0397 40 10.184 39.564C9.43139 39.1805 8.81947 38.5686 8.43597 37.816C8 36.9603 8 35.8402 8 33.6V30.4C8 28.1598 8 27.0397 8.43597 26.184C8.81947 25.4314 9.43139 24.8195 10.184 24.436C11.0397 24 12.1598 24 14.4 24H28.4617C30.5094 24 32 22.0476 32 20C32 11.1634 39.1634 4 48 4C56.8366 4 64 11.1634 64 20ZM48 24C50.2091 24 52 22.2091 52 20C52 17.7909 50.2091 16 48 16C45.7909 16 44 17.7909 44 20C44 22.2091 45.7909 24 48 24Z" fill="url(#paint0_linear_4064_903)"></path>
|
||||
<path d="M10.184 39.564L9.73005 40.455L10.184 39.564ZM8.43597 37.816L7.54497 38.27L8.43597 37.816ZM87.564 37.816L86.673 37.362L87.564 37.816ZM85.816 39.564L86.27 40.455L85.816 39.564ZM85.816 24.436L86.27 23.545L85.816 24.436ZM87.564 26.184L86.673 26.638L87.564 26.184ZM8.43597 26.184L9.32698 26.638L8.43597 26.184ZM81.6 23H67.5382V25H81.6V23ZM89 33.6V30.4H87V33.6H89ZM14.4 41H81.6V39H14.4V41ZM7 30.4V33.6H9V30.4H7ZM28.4617 23H14.4V25H28.4617V23ZM48 3C38.6112 3 31 10.6112 31 20H33C33 11.7157 39.7157 5 48 5V3ZM65 20C65 10.6112 57.3888 3 48 3V5C56.2843 5 63 11.7157 63 20H65ZM51 20C51 21.6569 49.6569 23 48 23V25C50.7614 25 53 22.7614 53 20H51ZM48 17C49.6569 17 51 18.3431 51 20H53C53 17.2386 50.7614 15 48 15V17ZM45 20C45 18.3431 46.3431 17 48 17V15C45.2386 15 43 17.2386 43 20H45ZM48 23C46.3431 23 45 21.6569 45 20H43C43 22.7614 45.2386 25 48 25V23ZM28.4617 25C31.2131 25 33 22.4353 33 20H31C31 21.66 29.8057 23 28.4617 23V25ZM14.4 39C13.2634 39 12.4711 38.9992 11.8542 38.9488C11.2491 38.8994 10.9014 38.8072 10.638 38.673L9.73005 40.455C10.3223 40.7568 10.9625 40.8826 11.6914 40.9422C12.4086 41.0008 13.2964 41 14.4 41V39ZM7 33.6C7 34.7036 6.99922 35.5914 7.05782 36.3086C7.11737 37.0375 7.24318 37.6777 7.54497 38.27L9.32698 37.362C9.19279 37.0986 9.10062 36.7509 9.05118 36.1458C9.00078 35.5289 9 34.7366 9 33.6H7ZM10.638 38.673C10.0735 38.3854 9.6146 37.9265 9.32698 37.362L7.54497 38.27C8.02433 39.2108 8.78924 39.9757 9.73005 40.455L10.638 38.673ZM87 33.6C87 34.7366 86.9992 35.5289 86.9488 36.1458C86.8994 36.7509 86.8072 37.0986 86.673 37.362L88.455 38.27C88.7568 37.6777 88.8826 37.0375 88.9422 36.3086C89.0008 35.5914 89 34.7036 89 33.6H87ZM81.6 41C82.7036 41 83.5914 41.0008 84.3086 40.9422C85.0375 40.8826 85.6777 40.7568 86.27 40.455L85.362 38.673C85.0986 38.8072 84.7509 38.8994 84.1458 38.9488C83.5289 38.9992 82.7366 39 81.6 39V41ZM86.673 37.362C86.3854 37.9265 85.9265 38.3854 85.362 38.673L86.27 40.455C87.2108 39.9757 87.9757 39.2108 88.455 38.27L86.673 37.362ZM81.6 25C82.7366 25 83.5289 25.0008 84.1458 25.0512C84.7509 25.1006 85.0986 25.1928 85.362 25.327L86.27 23.545C85.6777 23.2432 85.0375 23.1174 84.3086 23.0578C83.5914 22.9992 82.7036 23 81.6 23V25ZM89 30.4C89 29.2964 89.0008 28.4086 88.9422 27.6914C88.8826 26.9625 88.7568 26.3223 88.455 25.73L86.673 26.638C86.8072 26.9014 86.8994 27.2491 86.9488 27.8542C86.9992 28.4711 87 29.2634 87 30.4H89ZM85.362 25.327C85.9265 25.6146 86.3854 26.0735 86.673 26.638L88.455 25.73C87.9757 24.7892 87.2108 24.0243 86.27 23.545L85.362 25.327ZM9 30.4C9 29.2634 9.00078 28.4711 9.05118 27.8542C9.10062 27.2491 9.19279 26.9014 9.32698 26.638L7.54497 25.73C7.24318 26.3223 7.11737 26.9625 7.05782 27.6914C6.99922 28.4086 7 29.2964 7 30.4H9ZM14.4 23C13.2964 23 12.4085 22.9992 11.6914 23.0578C10.9625 23.1174 10.3223 23.2432 9.73005 23.545L10.638 25.327C10.9014 25.1928 11.2491 25.1006 11.8542 25.0512C12.4711 25.0008 13.2634 25 14.4 25V23ZM9.32698 26.638C9.6146 26.0735 10.0735 25.6146 10.638 25.327L9.73005 23.545C8.78924 24.0243 8.02433 24.7892 7.54497 25.73L9.32698 26.638ZM63 20C63 22.4353 64.7869 25 67.5382 25V23C66.1943 23 65 21.66 65 20H63Z" fill="url(#paint1_linear_4064_903)" mask="url(#path-1-inside-1_4064_903)"></path></g>
|
||||
<defs><filter id="filter0_di_4064_903" x="0" y="0" width="96" height="52" filterUnits="userSpaceOnUse" color-interpolation-filters="s-rGB"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix><feOffset dy="4"></feOffset><feGaussianBlur stdDeviation="4"></feGaussianBlur><feComposite in2="hardAlpha" operator="out"></feComposite><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"></feColorMatrix><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4064_903"></feBlend><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4064_903" result="shape"></feBlend><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix><feOffset dy="-1"></feOffset><feGaussianBlur stdDeviation="0.5"></feGaussianBlur><feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"></feComposite><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"></feColorMatrix><feBlend mode="normal" in2="shape" result="effect2_innerShadow_4064_903"></feBlend></filter><linearGradient id="paint0_linear_4064_903" x1="48" y1="4" x2="48" y2="40" gradientUnits="userSpaceOnUse"><stop stop-color="white"></stop><stop offset="0.6" stop-color="#CCCCCC"></stop><stop offset="1" stop-color="white"></stop></linearGradient><linearGradient id="paint1_linear_4064_903" x1="48" y1="4" x2="48" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="white"></stop><stop offset="1" stop-color="white" stop-opacity="0"></stop></linearGradient></defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative -mt-1 text-center text-base font-medium text-gray-800">Nestri needs your help</div>
|
||||
<div class="flex flex-row items-center justify-center gap-2 text-center text-sm text-white/80 [&>svg]:size-5">
|
||||
{profile.value && profile.value.avatarUrl ? (<img src={profile.value.avatarUrl} height={20} width={20} class="size-6 rounded-full" alt="Avatar" />) : (<Avatar name={profile.value ? `${profile.value.username}#${profile.value.discriminator}` : ""} />)}
|
||||
<span class="text-balance text-xs font-medium leading-tight text-black font-title">
|
||||
{profile.value && profile.value.username}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="relative flex items-center justify-center gap-4">
|
||||
<div class="h-[1px] flex-1 bg-gray-300/70" />
|
||||
<div class="text-center text-xs font-medium text-gray-500">What's wrong?</div>
|
||||
<div class="h-[1px] flex-1 bg-gray-300/70" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-start gap-4">
|
||||
<div class="flex size-4 shrink-0 items-center justify-center rounded-full bg-gray-200/70 text-xs font-semibold text-gray-400">1</div>
|
||||
<div class="text-xs text-gray-700">We're almost ready to launch Nestri, but server costs are our biggest hurdle right now.</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-start gap-4">
|
||||
<div class="flex size-4 shrink-0 items-center justify-center rounded-full bg-gray-200/70 text-xs font-semibold text-gray-400">2</div>
|
||||
<div class="text-xs text-gray-700">As a bootstrapped startup (yeah, just a few passionate developers!), we're reaching out to our early believers.</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-start gap-4">
|
||||
<div class="flex size-4 shrink-0 items-center justify-center rounded-full bg-gray-200/70 text-xs font-semibold text-gray-400">3</div>
|
||||
<div class="text-xs text-gray-700">Your early access subscription will directly fund our initial server infrastructure, helping us bring self-hosted cloud gaming to life.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex h-max w-max max-w-[380px] flex-col items-start gap-6">
|
||||
<div>
|
||||
<div class="h-12 text-center font-title text-lg font-medium text-white [text-shadow:0_4px_10px_rgba(0,87,255,.2),_0_-4px_10px_rgba(255,90,0,.15),_0_0_30px_rgba(255,255,255,.2)]" >
|
||||
What you get
|
||||
</div>
|
||||
<div class="flex w-full flex-col rounded-xl border border-none border-separator bg-white/20 bg-gradient-to-b from-black/20 to-black/30 pl-3 pr-5 shadow-lg dark:bg-white/[.09]">
|
||||
<div class="relative flex items-start justify-center gap-2.5 pt-3">
|
||||
<div class="flex size-5 shrink-0 items-center justify-center text-gray-200 font-bold tabular-nums">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="64" stroke-dashoffset="64" d="M8 3c0.5 0 2.5 4.5 2.5 5c0 1 -1.5 2 -2 3c-0.5 1 0.5 2 1.5 3c0.39 0.39 2 2 3 1.5c1 -0.5 2 -2 3 -2c0.5 0 5 2 5 2.5c0 2 -1.5 3.5 -3 4c-1.5 0.5 -2.5 0.5 -4.5 0c-2 -0.5 -3.5 -1 -6 -3.5c-2.5 -2.5 -3 -4 -3.5 -6c-0.5 -2 -0.5 -3 0 -4.5c0.5 -1.5 2 -3 4 -3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="64;0" /><animateTransform id="lineMdPhoneCallLoop0" fill="freeze" attributeName="transform" begin="0.6s;lineMdPhoneCallLoop0.begin+2.7s" dur="0.5s" type="rotate" values="0 12 12;15 12 12;0 12 12;-12 12 12;0 12 12;12 12 12;0 12 12;-15 12 12;0 12 12" /></path><path stroke-dasharray="4" stroke-dashoffset="4" d="M15.76 8.28c-0.5 -0.51 -1.1 -0.93 -1.76 -1.24M15.76 8.28c0.49 0.49 0.9 1.08 1.2 1.72"><animate fill="freeze" attributeName="stroke-dashoffset" begin="lineMdPhoneCallLoop0.begin+0s" dur="2.7s" keyTimes="0;0.111;0.259;0.37;1" values="4;0;0;4;4" /></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M18.67 5.35c-1 -1 -2.26 -1.73 -3.67 -2.1M18.67 5.35c0.99 1 1.72 2.25 2.08 3.65"><animate fill="freeze" attributeName="stroke-dashoffset" begin="lineMdPhoneCallLoop0.begin+0.2s" dur="2.7s" keyTimes="0;0.074;0.185;0.333;0.444;1" values="6;6;0;0;6;6" /></path></g></svg>
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center justify-start gap-0.5 border-b border-white/10 pb-3 pt-1 max-w-full truncate">
|
||||
<span class="text-sm leading-tight text-white truncate max-w-full">Schedule 1-on-1 calls with the Founders</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex items-start justify-center gap-2.5 pt-3">
|
||||
<div class="flex size-5 shrink-0 items-center justify-center text-gray-200 font-bold tabular-nums">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><g fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"><path d="m8.427 11.073l1.205-1.205a.4.4 0 0 0 .118-.285a.8.8 0 0 0-.236-.569L8.427 7.927a.603.603 0 0 0-.854 0L6.486 9.014a.8.8 0 0 0-.236.57c0 .106.042.208.118.284l1.205 1.205a.604.604 0 0 0 .854 0" /><path d="M16 5.796v-.028a1.768 1.768 0 0 0-3.018-1.25l-.76.76l-.024.024l-.374.374l-.415.415a.335.335 0 0 1-.561-.149l-.155-.566l-.139-.51l-.009-.033l-.65-2.386a1.964 1.964 0 0 0-3.79 0l-.65 2.386l-.01.032l-.139.511l-.154.566a.335.335 0 0 1-.56.15l-.416-.416l-.374-.374l-.024-.024l-.76-.76A1.768 1.768 0 0 0 0 5.768v.028q0 .203.046.403l1.3 5.631a1.4 1.4 0 0 0 .778.958a14.02 14.02 0 0 0 11.752 0c.394-.182.681-.535.779-.958l1.299-5.63q.045-.2.046-.404M3.53 7.152c.997.997 2.698.545 3.07-.815l.952-3.495a.464.464 0 0 1 .896 0L9.4 6.337c.37 1.36 2.072 1.812 3.068.815l1.574-1.574a.268.268 0 0 1 .457.19v.028a.3.3 0 0 1-.008.066l-1.288 5.584a12.52 12.52 0 0 1-10.408 0L1.508 5.862a.3.3 0 0 1-.008-.066v-.028a.268.268 0 0 1 .457-.19z" /></g></svg>
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center justify-start gap-0.5 border-b border-white/10 pb-3 pt-1 max-w-full truncate">
|
||||
<span class="text-sm leading-tight text-white truncate max-w-full">Keep your special early supporter pricing forever</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex items-start justify-center gap-2.5 pt-3">
|
||||
<div class="flex size-5 shrink-0 items-center justify-center text-gray-200 font-bold tabular-nums">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M6.818 22v-2.857C6.662 17.592 5.633 16.416 4.682 15m9.772 7v-1.714c4.91 0 4.364-5.714 4.364-5.714s2.182 0 2.182-2.286l-2.182-3.428c0-4.572-3.709-6.816-7.636-6.857c-2.2-.023-3.957.53-5.27 1.499" /><path d="m13 7l2 2.5l-2 2.5M5 7L3 9.5L5 12m5-6l-2 7" /></g></svg>
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center justify-start gap-0.5 border-b border-white/10 pb-3 pt-1">
|
||||
<span class="text-sm leading-tight text-white truncate max-w-full">Priority feature requests</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full justify-center items-center">
|
||||
<div class="mb-0.5 w-full flex items-center justify-center text-center text-sm leading-none text-gray-800" >Full access in</div>
|
||||
<div class="flex justify-center items-center gap-2 p-1">
|
||||
<TimeUnit value={timeLeft.days} label="Days" />
|
||||
<TimeUnit value={timeLeft.hours} label="Hours" />
|
||||
<TimeUnit value={timeLeft.minutes} label="Minutes" />
|
||||
<TimeUnit value={timeLeft.seconds} label="Seconds" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full justify-center flex">
|
||||
<div class="group relative w-full cursor-pointer">
|
||||
{/**https://sandbox-api.polar.sh/v1/checkout-links/polar_cl_3Kf9mOEl8We2ZnmYr0tolrFPfHiPvlC71XgZy4Jd2ni/redirect */}
|
||||
<Link href="https://buy.polar.sh/polar_cl_Y1SAbDjOOU8kbnKz4yUbF0Aqus94YA4by9FdS1I4igY" class="appearance-none outline-none scale-100 active:scale-[0.98] flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 px-4 text-sm font-semibold text-white transition-colors group-hover:bg-blue-400">Get early supporter price</Link>
|
||||
<div class="absolute -top-[22px] left-1/2 -translate-x-1/2">
|
||||
<Icons.specialOffer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
{/* <div class="absolute pointer-events-none size-full inset-0 dark:bg-[#0009] bg-[#b3b5b799] select-none backdrop-blur-[2px] z-50" /> */}
|
||||
</main >
|
||||
)
|
||||
})
|
||||
@@ -1,14 +1,14 @@
|
||||
import type Nestri from "@nestri/sdk"
|
||||
// import type Nestri from "@nestri/sdk"
|
||||
import { component$, Slot } from "@builder.io/qwik";
|
||||
import { type RequestHandler } from "@builder.io/qwik-city";
|
||||
// import { type RequestHandler } from "@builder.io/qwik-city";
|
||||
|
||||
export const onRequest: RequestHandler = async ({ url, redirect, sharedMap }) => {
|
||||
const currentProfile = sharedMap.get("profile") as Nestri.Users.UserRetrieveResponse.Data | null
|
||||
// export const onRequest: RequestHandler = async ({ url, redirect, sharedMap }) => {
|
||||
// // const currentProfile = sharedMap.get("profile") as Nestri.Users.UserRetrieveResponse.Data | null
|
||||
|
||||
if (!currentProfile) {
|
||||
throw redirect(308, `${url.origin}`)
|
||||
}
|
||||
}
|
||||
// // if (!currentProfile) {
|
||||
// // throw redirect(308, `${url.origin}`)
|
||||
// // }
|
||||
// }
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
|
||||
@@ -1,328 +1,309 @@
|
||||
// import posthog from "posthog-js";
|
||||
import { Modal } from "@nestri/ui";
|
||||
import { useLocation } from "@builder.io/qwik-city";
|
||||
import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"
|
||||
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { $, component$, noSerialize, type NoSerialize, useSignal, useStore, useVisibleTask$ } from "@builder.io/qwik";
|
||||
// import Nestri from "@nestri/sdk";
|
||||
|
||||
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
|
||||
// FIXME: Add authentication and authorization
|
||||
// export const getUserSubscriptions = server$(
|
||||
// async function () {
|
||||
|
||||
// const access = this.cookie.get("access_token")
|
||||
// if (access) {
|
||||
// const bearerToken = access.value
|
||||
|
||||
// const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
|
||||
// const subscriptions = await nestriClient.users.session()
|
||||
|
||||
// return subscriptions as "Free" | "Pro"
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
type PlayState = {
|
||||
nestriMouse: NoSerialize<Mouse | undefined>
|
||||
nestriKeyboard: NoSerialize<Keyboard | undefined>
|
||||
webrtc: NoSerialize<WebRTCStream | undefined>
|
||||
nestriLock?: boolean
|
||||
hasStream?: boolean
|
||||
showOffline?: boolean
|
||||
video?: HTMLVideoElement
|
||||
inputInitialized?: boolean
|
||||
initializedVideo?: boolean
|
||||
}
|
||||
|
||||
export default component$(() => {
|
||||
const id = useLocation().params.id;
|
||||
const showBannerModal = useSignal(false)
|
||||
const showButtonModal = useSignal(false)
|
||||
const canvas = useSignal<HTMLCanvasElement>();
|
||||
const playState = useStore<PlayState>({
|
||||
nestriMouse: undefined,
|
||||
nestriKeyboard: undefined,
|
||||
nestriLock: undefined,
|
||||
webrtc: undefined,
|
||||
video: undefined,
|
||||
hasStream: undefined,
|
||||
showOffline: undefined,
|
||||
inputInitialized: false,
|
||||
initializedVideo: false
|
||||
})
|
||||
|
||||
const initializeInputDevices = $(() => {
|
||||
if (!canvas.value || !playState.webrtc || playState.inputInitialized) return;
|
||||
|
||||
try {
|
||||
playState.nestriMouse = noSerialize(new Mouse({
|
||||
canvas: canvas.value,
|
||||
webrtc: playState.webrtc
|
||||
}));
|
||||
playState.nestriKeyboard = noSerialize(new Keyboard({
|
||||
canvas: canvas.value,
|
||||
webrtc: playState.webrtc
|
||||
}));
|
||||
playState.inputInitialized = true;
|
||||
console.log("Input devices initialized successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize input devices:", error);
|
||||
playState.inputInitialized = false;
|
||||
}
|
||||
});
|
||||
|
||||
const lockPlay = $(async () => {
|
||||
if (!canvas.value || !playState.hasStream) return;
|
||||
|
||||
try {
|
||||
await canvas.value.requestPointerLock();
|
||||
await canvas.value.requestFullscreen();
|
||||
|
||||
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);
|
||||
playState.nestriLock = true;
|
||||
console.log("Keyboard lock acquired");
|
||||
} catch (e) {
|
||||
console.warn("Keyboard lock failed:", e);
|
||||
playState.nestriLock = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during lock sequence:", error);
|
||||
}
|
||||
});
|
||||
|
||||
const setupPointerLockListener = $(() => {
|
||||
document.addEventListener("pointerlockchange", () => {
|
||||
if (!canvas.value) return;
|
||||
|
||||
if (document.pointerLockElement === canvas.value) {
|
||||
// Initialize input devices when pointer is locked
|
||||
if (!playState.inputInitialized) {
|
||||
initializeInputDevices();
|
||||
}
|
||||
} else {
|
||||
if (!showBannerModal.value) {
|
||||
const playing = sessionStorage.getItem("showedBanner");
|
||||
showBannerModal.value = !playing || playing !== "true";
|
||||
showButtonModal.value = playing === "true";
|
||||
}
|
||||
|
||||
// Clean up input devices
|
||||
if (playState.nestriKeyboard) {
|
||||
playState.nestriKeyboard.dispose();
|
||||
playState.nestriKeyboard = undefined;
|
||||
}
|
||||
if (playState.nestriMouse) {
|
||||
playState.nestriMouse.dispose();
|
||||
playState.nestriMouse = undefined;
|
||||
}
|
||||
playState.nestriLock = undefined;
|
||||
playState.inputInitialized = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleVideoInput = $(async () => {
|
||||
if (!playState.video) return;
|
||||
if (playState.initializedVideo) return;
|
||||
|
||||
await playState.video.play().then(() => {
|
||||
if (canvas.value && playState.video) {
|
||||
canvas.value.width = playState.video.videoWidth;
|
||||
canvas.value.height = playState.video.videoHeight;
|
||||
playState.initializedVideo = true
|
||||
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
const renderer = () => {
|
||||
if (ctx && playState.hasStream && playState.video) {
|
||||
ctx.drawImage(playState.video, 0, 0);
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
};
|
||||
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Error playing video:", error);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => canvas.value);
|
||||
|
||||
if (!canvas.value) return; // Ensure canvas is available
|
||||
|
||||
// Create video element and make it output to canvas (TODO: improve this)
|
||||
let video = document.getElementById("webrtc-video-player");
|
||||
if (!video) {
|
||||
video = document.createElement("video");
|
||||
video.id = "stream-video-player";
|
||||
video.style.visibility = "hidden";
|
||||
const webrtc = new WebRTCStream("https://relay.dathorse.com", id, (mediaStream) => {
|
||||
if (video && mediaStream && (video as HTMLVideoElement).srcObject === null) {
|
||||
console.log("Setting mediastream");
|
||||
(video as HTMLVideoElement).srcObject = mediaStream;
|
||||
setupPointerLockListener();
|
||||
try {
|
||||
if (!playState.video) {
|
||||
playState.video = document.createElement("video") as HTMLVideoElement
|
||||
playState.video.style.visibility = "hidden";
|
||||
playState.webrtc = noSerialize(new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => {
|
||||
if (playState.video && mediaStream && playState.video.srcObject === null) {
|
||||
console.log("Setting mediastream");
|
||||
playState.video.srcObject = mediaStream;
|
||||
playState.hasStream = true;
|
||||
playState.showOffline = false;
|
||||
|
||||
// @ts-ignore
|
||||
window.hasstream = true;
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement?.remove();
|
||||
// @ts-ignore
|
||||
window.playbtnelement?.remove();
|
||||
const playing = sessionStorage.getItem("showedBanner")
|
||||
if (!playing || playing != "true") {
|
||||
if (!showBannerModal.value) showBannerModal.value = true
|
||||
} else {
|
||||
if (!showButtonModal.value) showButtonModal.value = true
|
||||
}
|
||||
|
||||
const playbtn = document.createElement("button");
|
||||
playbtn.style.position = "absolute";
|
||||
playbtn.style.left = "50%";
|
||||
playbtn.style.top = "50%";
|
||||
playbtn.style.transform = "translateX(-50%) translateY(-50%)";
|
||||
playbtn.style.width = "12rem";
|
||||
playbtn.style.height = "6rem";
|
||||
playbtn.style.borderRadius = "1rem";
|
||||
playbtn.style.backgroundColor = "rgb(175, 50, 50)";
|
||||
playbtn.style.color = "black";
|
||||
playbtn.style.fontSize = "1.5em";
|
||||
playbtn.textContent = "< Start >";
|
||||
await handleVideoInput();
|
||||
} else if (mediaStream === null) {
|
||||
console.log("MediaStream is null, Room is offline");
|
||||
playState.showOffline = true
|
||||
playState.hasStream = false;
|
||||
// Clear canvas if it has been set
|
||||
if (canvas.value) {
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
}
|
||||
} else if (playState.video && playState.video.srcObject !== null) {
|
||||
console.log("Setting new mediastream");
|
||||
playState.video.srcObject = mediaStream;
|
||||
playState.hasStream = true;
|
||||
playState.showOffline = true
|
||||
|
||||
playbtn.onclick = () => {
|
||||
playbtn.remove();
|
||||
(video as HTMLVideoElement).play().then(() => {
|
||||
if (canvas.value) {
|
||||
canvas.value.width = (video as HTMLVideoElement).videoWidth;
|
||||
canvas.value.height = (video as HTMLVideoElement).videoHeight;
|
||||
playState.video.play().then(() => {
|
||||
// window.roomOfflineElement?.remove();
|
||||
playState.showOffline = false
|
||||
if (canvas.value && playState.video) {
|
||||
canvas.value.width = playState.video.videoWidth;
|
||||
canvas.value.height = playState.video.videoHeight;
|
||||
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
const renderer = () => {
|
||||
// @ts-ignore
|
||||
if (ctx && window.hasstream) {
|
||||
ctx.drawImage((video as HTMLVideoElement), 0, 0);
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
if (ctx && playState.hasStream && playState.video) {
|
||||
ctx.drawImage(playState.video, 0, 0);
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
}
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("pointerlockchange", () => {
|
||||
if (!canvas.value) return; // Ensure canvas is available
|
||||
// @ts-ignore
|
||||
if (document.pointerLockElement && !window.nestrimouse && !window.nestrikeyboard) {
|
||||
// @ts-ignore
|
||||
window.nestrimouse = new Mouse({ canvas: canvas.value, webrtc });
|
||||
// @ts-ignore
|
||||
window.nestrikeyboard = new Keyboard({ canvas: canvas.value, webrtc });
|
||||
// @ts-ignore
|
||||
} else if (!document.pointerLockElement && window.nestrimouse && window.nestrikeyboard) {
|
||||
// @ts-ignore
|
||||
window.nestrimouse.dispose();
|
||||
// @ts-ignore
|
||||
window.nestrimouse = undefined;
|
||||
// @ts-ignore
|
||||
window.nestrikeyboard.dispose();
|
||||
// @ts-ignore
|
||||
window.nestrikeyboard = undefined;
|
||||
// @ts-ignore
|
||||
window.nestriLock = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
document.body.append(playbtn);
|
||||
// @ts-ignore
|
||||
window.playbtnelement = playbtn;
|
||||
} else if (mediaStream === null) {
|
||||
console.log("MediaStream is null, Room is offline");
|
||||
// @ts-ignore
|
||||
window.playbtnelement?.remove();
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement?.remove();
|
||||
// Add a message to the screen
|
||||
const offline = document.createElement("div");
|
||||
offline.style.position = "absolute";
|
||||
offline.style.left = "50%";
|
||||
offline.style.top = "50%";
|
||||
offline.style.transform = "translateX(-50%) translateY(-50%)";
|
||||
offline.style.width = "auto";
|
||||
offline.style.height = "auto";
|
||||
offline.style.color = "lightgray";
|
||||
offline.style.fontSize = "2em";
|
||||
offline.textContent = "Offline";
|
||||
document.body.append(offline);
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement = offline;
|
||||
// @ts-ignore
|
||||
window.hasstream = false;
|
||||
// Clear canvas if it has been set
|
||||
if (canvas.value) {
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
}
|
||||
} else if ((video as HTMLVideoElement).srcObject !== null) {
|
||||
console.log("Setting new mediastream");
|
||||
(video as HTMLVideoElement).srcObject = mediaStream;
|
||||
// @ts-ignore
|
||||
window.hasstream = true;
|
||||
// Start video rendering
|
||||
(video as HTMLVideoElement).play().then(() => {
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement?.remove();
|
||||
if (canvas.value) {
|
||||
canvas.value.width = (video as HTMLVideoElement).videoWidth;
|
||||
canvas.value.height = (video as HTMLVideoElement).videoHeight;
|
||||
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
const renderer = () => {
|
||||
// @ts-ignore
|
||||
if (ctx && window.hasstream) {
|
||||
ctx.drawImage((video as HTMLVideoElement), 0, 0);
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
}
|
||||
}
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error handling the media connection", error)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvas}
|
||||
onClick$={async () => {
|
||||
// @ts-ignore
|
||||
if (canvas.value && window.hasstream && !window.nestriLock) {
|
||||
// Do not use - unadjustedMovement: true - breaks input on linux
|
||||
await canvas.value.requestPointerLock();
|
||||
await canvas.value.requestFullscreen()
|
||||
if (document.fullscreenElement !== null) {
|
||||
// @ts-ignore
|
||||
if ('keyboard' in window.navigator && 'lock' in window.navigator.keyboard) {
|
||||
const keys = [
|
||||
"AltLeft",
|
||||
"AltRight",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"ContextMenu",
|
||||
"MetaLeft",
|
||||
"MetaRight"
|
||||
];
|
||||
console.log("requesting keyboard lock");
|
||||
// @ts-ignore
|
||||
window.navigator.keyboard.lock(keys).then(
|
||||
() => {
|
||||
console.log("keyboard lock success");
|
||||
// @ts-ignore
|
||||
window.nestriLock = true;
|
||||
}
|
||||
).catch(
|
||||
(e: any) => {
|
||||
console.log("keyboard lock failed: ", e);
|
||||
// @ts-ignore
|
||||
window.nestriLock = false;
|
||||
}
|
||||
)
|
||||
} else {
|
||||
console.log("keyboard lock not supported, navigator is: ", window.navigator, navigator);
|
||||
// @ts-ignore
|
||||
window.nestriLock = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
|
||||
class="aspect-video h-full w-full object-contain max-h-screen" />
|
||||
<>
|
||||
{playState.showOffline ? (
|
||||
<div class="w-screen h-screen flex justify-center items-center">
|
||||
<span class="text-2xl font-semibold flex items-center gap-2" >
|
||||
Offline
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
< canvas
|
||||
ref={canvas}
|
||||
onClick$={lockPlay}
|
||||
class="aspect-video h-full w-full object-contain max-h-screen" />
|
||||
{typeof playState.showOffline === "undefined" && (
|
||||
<div class="w-screen h-screen bg-gray-100 dark:bg-gray-900 absolute z-10 flex justify-center items-center">
|
||||
<span class="text-xl font-semibold flex items-center gap-2" >
|
||||
<div data-component="spinner">
|
||||
<div>
|
||||
{new Array(12).fill(0).map((i, k) => (
|
||||
<div key={k} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
Warming up the GPU...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Modal.Root bind:show={showButtonModal} closeOnBackdropClick={false}>
|
||||
<Modal.Panel class="
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[370px] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
|
||||
dark:[box-shadow: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] dark:bg-[#222b]
|
||||
[box-shadow: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] bg-[#fffd]
|
||||
backdrop-blur-lg py-4 px-5 modal" >
|
||||
<div class="size-full flex flex-col">
|
||||
<div class="flex flex-col gap-3" >
|
||||
<button
|
||||
onClick$={async () => {
|
||||
showButtonModal.value = false;
|
||||
await handleVideoInput()
|
||||
await lockPlay();
|
||||
}}
|
||||
class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4" >
|
||||
Continue Playing
|
||||
</button>
|
||||
<button class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4" >
|
||||
Shutdown Nestri
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
<Modal.Root bind:show={showBannerModal} closeOnBackdropClick={false}>
|
||||
<Modal.Panel class="
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[37%] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
|
||||
dark:[box-shadow: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] dark:bg-[#222b]
|
||||
[box-shadow: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] bg-[#fffd]
|
||||
backdrop-blur-lg py-4 px-5 modal" >
|
||||
<div class="size-full flex flex-col">
|
||||
<div class="dark:text-white text-black">
|
||||
<h3 class="font-semibold text-2xl tracking-tight mb-2 font-title">Important information from Nestri</h3>
|
||||
<div class="text-sm dark:text-white/[.79] text-[rgba(19,21,23,0.64)]" >
|
||||
This product is in Alpha — please share feedback whenever possible to help us improve. Thanks you for your support! 💖
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:pt-10 sm:block hidden" >
|
||||
<button
|
||||
onClick$={async () => {
|
||||
sessionStorage.setItem("showedBanner", "true");
|
||||
showBannerModal.value = false;
|
||||
await handleVideoInput()
|
||||
await lockPlay();
|
||||
}}
|
||||
class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:hover:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] focus:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] font-medium font-title rounded-lg flex h-[calc(2.25rem+2*1px)] flex-col text-white w-full leading-none truncate bg-primary-500 items-center justify-center" >
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
{/**
|
||||
.spinningCircleInner_b6db20 {
|
||||
transform: rotate(280deg);
|
||||
}
|
||||
.inner_b6db20 {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
contain: paint;
|
||||
} */
|
||||
}
|
||||
|
||||
{/* <div class="loadingPopout_a8c724" role="dialog" tabindex="-1" aria-modal="true"><div class="spinner_b6db20 spinningCircle_b6db20" role="img" aria-label="Loading"><div class="spinningCircleInner_b6db20 inner_b6db20"><svg class="circular_b6db20" viewBox="25 25 50 50"><circle class="path_b6db20 path3_b6db20" cx="50" cy="50" r="20"></circle><circle class="path_b6db20 path2_b6db20" cx="50" cy="50" r="20"></circle><circle class="path_b6db20" cx="50" cy="50" r="20"></circle></svg></div></div></div> */
|
||||
}
|
||||
// .loadingPopout_a8c724 {
|
||||
// background-color: var(--background-secondary);
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// padding: 8px;
|
||||
// }
|
||||
|
||||
// .circular_b6db20 {
|
||||
// animation: spinner-spinning-circle-rotate_b6db20 2s linear infinite;
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
// }
|
||||
|
||||
// 100% {
|
||||
// transform: rotate(360deg);
|
||||
// }
|
||||
|
||||
|
||||
{/* .path3_b6db20 {
|
||||
animation-delay: .23s;
|
||||
stroke: var(--text-brand);
|
||||
}
|
||||
.path_b6db20 {
|
||||
animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite;
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--brand-500);
|
||||
}
|
||||
circle[Attributes Style] {
|
||||
cx: 50;
|
||||
cy: 50;
|
||||
r: 20;
|
||||
}
|
||||
user agent stylesheet
|
||||
:not(svg) {
|
||||
transform-origin: 0px 0px;
|
||||
} */
|
||||
}
|
||||
|
||||
|
||||
// .path2_b6db20 {
|
||||
// animation-delay: .15s;
|
||||
// stroke: var(--text-brand);
|
||||
// opacity: .6;
|
||||
// }
|
||||
// .path_b6db20 {
|
||||
// animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite;
|
||||
// stroke-dasharray: 1, 200;
|
||||
// stroke-dashoffset: 0;
|
||||
// fill: none;
|
||||
// stroke-width: 6;
|
||||
// stroke-miterlimit: 10;
|
||||
// stroke-linecap: round;
|
||||
// stroke: var(--brand-500);
|
||||
// }
|
||||
// circle[Attributes Style] {
|
||||
// cx: 50;
|
||||
// cy: 50;
|
||||
// r: 20;
|
||||
|
||||
|
||||
// function throttle(func, limit) {
|
||||
// let inThrottle;
|
||||
// return function(...args) {
|
||||
// if (!inThrottle) {
|
||||
// func.apply(this, args);
|
||||
// inThrottle = true;
|
||||
// setTimeout(() => inThrottle = false, limit);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Use it like this:
|
||||
// const throttledMouseMove = throttle((x, y) => {
|
||||
// websocket.send(JSON.stringify({
|
||||
// type: 'mousemove',
|
||||
// x: x,
|
||||
// y: y
|
||||
// }));
|
||||
// }, 16); // ~60fps
|
||||
|
||||
// use std::time::Instant;
|
||||
|
||||
// // Add these to your AppState
|
||||
// struct AppState {
|
||||
// pipeline: Arc<Mutex<gst::Pipeline>>,
|
||||
// last_mouse_move: Arc<Mutex<(i32, i32, Instant)>>, // Add this
|
||||
// }
|
||||
|
||||
// // Then in your MouseMove handler:
|
||||
// InputMessage::MouseMove { x, y } => {
|
||||
// let mut last_move = state.last_mouse_move.lock().unwrap();
|
||||
// let now = Instant::now();
|
||||
|
||||
// // Only process if coordinates are different or enough time has passed
|
||||
// if (last_move.0 != x || last_move.1 != y) &&
|
||||
// (now.duration_since(last_move.2).as_millis() > 16) { // ~60fps
|
||||
|
||||
// println!("Mouse moved to x: {}, y: {}", x, y);
|
||||
|
||||
// let structure = gst::Structure::builder("MouseMoveRelative")
|
||||
// .field("pointer_x", x as f64)
|
||||
// .field("pointer_y", y as f64)
|
||||
// .build();
|
||||
|
||||
// let event = gst::event::CustomUpstream::new(structure);
|
||||
// pipeline.send_event(event);
|
||||
|
||||
// // Update last position and time
|
||||
// *last_move = (x, y, now);
|
||||
// }
|
||||
// }
|
||||
})
|
||||
@@ -1,13 +1,13 @@
|
||||
.blog h1 {
|
||||
@apply text-4xl font-title font-bold text-gray-800 dark:text-gray-200;
|
||||
@apply text-4xl font-bricolage font-bold text-gray-800 dark:text-gray-200;
|
||||
}
|
||||
|
||||
.blog h2 {
|
||||
@apply text-3xl font-title font-bold text-gray-800 dark:text-gray-200;
|
||||
@apply text-3xl font-bricolage font-bold text-gray-800 dark:text-gray-200;
|
||||
}
|
||||
|
||||
.blog h3 {
|
||||
@apply text-2xl font-title font-bold text-gray-800 dark:text-gray-200;
|
||||
@apply text-2xl font-mona font-bold text-gray-800 dark:text-gray-200;
|
||||
}
|
||||
|
||||
.blog img {
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
.blog strong {
|
||||
@apply text-gray-700 dark:text-gray-300 font-title font-semibold;
|
||||
@apply text-gray-700 dark:text-gray-300 font-mona font-semibold;
|
||||
}
|
||||
|
||||
.blog ul {
|
||||
|
||||
@@ -30,7 +30,7 @@ export default component$(() => {
|
||||
<Link key={blog.title} class="border-b border-gray-300 dark:border-gray-700 outline-none w-full" href={`/blog/${blog.href}`}>
|
||||
<div class="w-full gap-3 py-6 hover:px-2 flex relative items-center rounded-md hover:bg-gray-200 dark:hover:bg-gray-800 transition-all duration-200">
|
||||
<div class="w-max flex flex-col max-w-[70%]">
|
||||
<h2 class="text-lg inline-block font-title font-bold dark:text-gray-100 text-gray-800">{blog.title}</h2>
|
||||
<h2 class="text-lg inline-block font-bricolage font-bold dark:text-gray-100 text-gray-800">{blog.title}</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 overflow-ellipsis whitespace-nowrap overflow-hidden">{blog.description}</p>
|
||||
</div>
|
||||
<div class="flex-1 relative min-w-[8px] box-border before:absolute before:-bottom-[1px] before:h-[1px] before:w-full before:bg-gray-600 dark:before:bg-gray-400 before:z-[5] before:duration-300 before:transition-all" />
|
||||
|
||||
@@ -57,7 +57,7 @@ export default component$(() => {
|
||||
</nav>
|
||||
</div>
|
||||
<div class="max-w-xl mx-auto w-full gap-1 flex flex-col">
|
||||
<h2 class="text-4xl text-start tracking-tight font-bold font-title">
|
||||
<h2 class="text-4xl text-start tracking-tight font-bold font-bricolage">
|
||||
{frontmatter.blogTitle}
|
||||
</h2>
|
||||
<div class="list-none flex flex-col items-start justify-start mt-2 gap-2 text-sm w-full">
|
||||
|
||||
@@ -64,7 +64,7 @@ export default component$(() => {
|
||||
</div>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
<Footer showBanner={false} />
|
||||
<Footer/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,50 +1,10 @@
|
||||
// import Nestri from "@nestri/sdk";
|
||||
import { NavBar } from "@nestri/ui";
|
||||
import { component$, Slot } from "@builder.io/qwik";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { type RequestHandler, routeLoader$ } from "@builder.io/qwik-city";
|
||||
|
||||
//FIXME: This seems not to work
|
||||
// export const onRequest: RequestHandler = async ({ cookie, sharedMap }) => {
|
||||
// const access = cookie.get("access_token")
|
||||
// if (access) {
|
||||
// const bearerToken = access.value
|
||||
|
||||
// const nestriClient = new Nestri({
|
||||
// bearerToken,
|
||||
// baseURL: "https://api.lauryn.dev.nestri.io"
|
||||
// })
|
||||
|
||||
// const currentProfile = await nestriClient.users.retrieve()
|
||||
// sharedMap.set("profile", currentProfile.data)
|
||||
// }
|
||||
// }
|
||||
|
||||
export const onRequest: RequestHandler = async ({url, sharedMap }) => {
|
||||
// const access = cookie.get("access_token")
|
||||
// if (!access) {
|
||||
const client = createClient({
|
||||
clientID: "www",
|
||||
issuer: "https://auth.nestri.io"
|
||||
})
|
||||
|
||||
const auth = await client.authorize(url.origin + "/callback", "code")
|
||||
sharedMap.set("auth_url", auth.url)
|
||||
// }
|
||||
}
|
||||
|
||||
export const useLink = routeLoader$(async ({sharedMap}) => {
|
||||
const url = sharedMap.get("auth_url") as string
|
||||
|
||||
return url
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
const loginUrl = useLink()
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar link={loginUrl.value} />
|
||||
<NavBar />
|
||||
<Slot />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ export default component$(() => {
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="w-screen relative">
|
||||
<TitleSection client:load title="Pricing" description={"We're growing at the speed of trust. Choose a price that feels right for you and help support Nestri"} />
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
@@ -519,7 +519,21 @@ export default component$(() => {
|
||||
</section>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
<Footer />
|
||||
</>
|
||||
<Footer client:load>
|
||||
<div class="w-full flex justify-center flex-col items-center gap-3">
|
||||
<Link href="/auth/login" prefetch={false} class="flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
|
||||
Get early access
|
||||
</Link>
|
||||
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
<span class="hover:text-primary-500 transition-colors duration-200">
|
||||
<Link rel="noreferrer" href="/terms" >Terms of Service</Link></span>
|
||||
<span class="text-gray-400 dark:text-gray-600">•</span>
|
||||
<span class="hover:text-primary-500 transition-colors duration-200" >
|
||||
<Link href="/privacy">Privacy Policy</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Footer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import posthog from "posthog-js";
|
||||
import Nestri from "@nestri/sdk";
|
||||
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { routeLoader$, useNavigate, type CookieOptions } from "@builder.io/qwik-city";
|
||||
|
||||
export const useLoggedIn = routeLoader$(async ({ query, url, cookie }) => {
|
||||
@@ -9,7 +10,7 @@ export const useLoggedIn = routeLoader$(async ({ query, url, cookie }) => {
|
||||
const redirect_uri = url.origin + "/callback"
|
||||
const cookieOptions: CookieOptions = {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
sameSite: "lax",
|
||||
secure: false, // Only send cookies over HTTPS
|
||||
//FIXME: This causes weird issues in Qwik
|
||||
httpOnly: true, // Prevent JavaScript access to cookies
|
||||
@@ -38,23 +39,33 @@ export const useLoggedIn = routeLoader$(async ({ query, url, cookie }) => {
|
||||
|
||||
//TODO: Use subjects instead
|
||||
const currentProfile = await nestriClient.users.retrieve()
|
||||
const username = currentProfile.data.username
|
||||
return username
|
||||
const userProfile = currentProfile.data
|
||||
return userProfile
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
const username = useLoggedIn()
|
||||
const userProfile = useLoggedIn()
|
||||
const navigate = useNavigate();
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(() => {
|
||||
if (username.value) {
|
||||
setTimeout(async () => {
|
||||
await navigate(`${window.location.origin}/${username.value}`)
|
||||
}, 500);
|
||||
|
||||
if (userProfile.value) {
|
||||
posthog.identify(
|
||||
userProfile.value.id,
|
||||
{
|
||||
username: userProfile.value.username,
|
||||
joinedAt: userProfile.value.createdAt,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await navigate(`${window.location.origin}/home`)
|
||||
}, 500);
|
||||
})
|
||||
|
||||
return (
|
||||
13
apps/www/src/routes/auth/layout.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { type RequestHandler } from "@builder.io/qwik-city";
|
||||
|
||||
export const onRequest: RequestHandler = async ({ json, url }) => {
|
||||
// const access = cookie.get("access_token")
|
||||
// if (!access) {
|
||||
// throw json(401, { error: "You are not authorized" })
|
||||
// }
|
||||
|
||||
if (!url.hostname.endsWith("nestri.io") && !url.hostname.endsWith("localhost")) {
|
||||
throw json(404, { error: "We could not serve your request" })
|
||||
}
|
||||
|
||||
}
|
||||
16
apps/www/src/routes/auth/login/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { type RequestHandler } from "@builder.io/qwik-city";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
|
||||
export const onGet: RequestHandler = async ({ cookie, redirect, url }) => {
|
||||
cookie.delete("access_token")
|
||||
cookie.delete("refresh_token")
|
||||
|
||||
const client = createClient({
|
||||
clientID: "www",
|
||||
issuer: "https://auth.nestri.io"
|
||||
})
|
||||
|
||||
const auth = await client.authorize(url.origin + "/auth/callback", "code")
|
||||
|
||||
throw redirect(308, auth.url)
|
||||
}
|
||||
7
apps/www/src/routes/auth/logout/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { type RequestHandler } from "@builder.io/qwik-city";
|
||||
|
||||
export const onGet: RequestHandler = async ({ cookie, redirect, url }) => {
|
||||
cookie.delete("access_token")
|
||||
cookie.delete("refresh_token")
|
||||
throw redirect(308, url.origin)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import posthog from "posthog-js";
|
||||
import Nestri from "@nestri/sdk";
|
||||
import { NavProgress } from "@nestri/ui";
|
||||
import { component$, Slot, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { type DocumentHead, type RequestHandler } from "@builder.io/qwik-city";
|
||||
@@ -15,29 +14,6 @@ export const onGet: RequestHandler = async ({ cacheControl }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const onRequest: RequestHandler = async ({ cookie, url, redirect, sharedMap }) => {
|
||||
const access = cookie.get("access_token")
|
||||
if (access) {
|
||||
try {
|
||||
|
||||
const bearerToken = access.value
|
||||
|
||||
const nestriClient = new Nestri({
|
||||
bearerToken,
|
||||
baseURL: "https://api.nestri.io"
|
||||
})
|
||||
const currentProfile = await nestriClient.users.retrieve()
|
||||
sharedMap.set("profile", currentProfile.data)
|
||||
} catch (error) {
|
||||
console.log("error working with bearer token", error)
|
||||
cookie.delete("access_token")
|
||||
cookie.delete("refresh_token")
|
||||
|
||||
throw redirect(302, url.origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default component$(() => {
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(() => {
|
||||
|
||||
6
apps/www/src/routes/links/github/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type RequestHandler } from "@builder.io/qwik-city"
|
||||
|
||||
export const onGet: RequestHandler = async ({ redirect }) => {
|
||||
|
||||
throw redirect(308, "https://github.com/nestrilabs/nestri")
|
||||
}
|
||||
@@ -1,303 +1,222 @@
|
||||
import { useLocation } from "@builder.io/qwik-city";
|
||||
import {WebRTCStream } from "@nestri/input"
|
||||
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
// import posthog from "posthog-js";
|
||||
import { Modal } from "@nestri/ui";
|
||||
import { WebRTCStream } from "@nestri/input"
|
||||
import { useLocation, useNavigate } from "@builder.io/qwik-city";
|
||||
import { $, component$, noSerialize, type NoSerialize, useSignal, useStore, useVisibleTask$ } from "@builder.io/qwik";
|
||||
|
||||
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
|
||||
|
||||
type PlayState = {
|
||||
webrtc: NoSerialize<WebRTCStream | undefined>
|
||||
nestriLock?: boolean
|
||||
hasStream?: boolean
|
||||
showOffline?: boolean
|
||||
video?: HTMLVideoElement
|
||||
inputInitialized?: boolean
|
||||
initializedVideo?: boolean
|
||||
}
|
||||
|
||||
export default component$(() => {
|
||||
const id = useLocation().params.id;
|
||||
const nav = useNavigate()
|
||||
const showBannerModal = useSignal(false)
|
||||
const showButtonModal = useSignal(false)
|
||||
const canvas = useSignal<HTMLCanvasElement>();
|
||||
const playState = useStore<PlayState>({
|
||||
nestriLock: undefined,
|
||||
webrtc: undefined,
|
||||
video: undefined,
|
||||
hasStream: undefined,
|
||||
showOffline: undefined,
|
||||
inputInitialized: false,
|
||||
initializedVideo: false
|
||||
})
|
||||
|
||||
const lockPlay = $(async () => {
|
||||
if (!canvas.value || !playState.hasStream) return;
|
||||
|
||||
try {
|
||||
await canvas.value.requestFullscreen();
|
||||
} catch (error) {
|
||||
console.error("Error during lock sequence:", error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleVideoInput = $(async () => {
|
||||
if (!playState.video) return;
|
||||
if (playState.initializedVideo) return;
|
||||
|
||||
await playState.video.play().then(() => {
|
||||
if (canvas.value && playState.video) {
|
||||
canvas.value.width = playState.video.videoWidth;
|
||||
canvas.value.height = playState.video.videoHeight;
|
||||
playState.initializedVideo = true
|
||||
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
const renderer = () => {
|
||||
if (ctx && playState.hasStream && playState.video) {
|
||||
ctx.drawImage(playState.video, 0, 0);
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
};
|
||||
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Error playing video:", error);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => canvas.value);
|
||||
|
||||
if (!canvas.value) return; // Ensure canvas is available
|
||||
|
||||
// Create video element and make it output to canvas (TODO: improve this)
|
||||
let video = document.getElementById("webrtc-video-player");
|
||||
if (!video) {
|
||||
video = document.createElement("video");
|
||||
video.id = "stream-video-player";
|
||||
video.style.visibility = "hidden";
|
||||
new WebRTCStream("https://relay.dathorse.com", id, (mediaStream) => {
|
||||
if (video && mediaStream && (video as HTMLVideoElement).srcObject === null) {
|
||||
console.log("Setting mediastream");
|
||||
(video as HTMLVideoElement).srcObject = mediaStream;
|
||||
try {
|
||||
if (!playState.video) {
|
||||
playState.video = document.createElement("video") as HTMLVideoElement
|
||||
playState.video.style.visibility = "hidden";
|
||||
playState.webrtc = noSerialize(new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => {
|
||||
if (playState.video && mediaStream && playState.video.srcObject === null) {
|
||||
console.log("Setting mediastream");
|
||||
playState.video.srcObject = mediaStream;
|
||||
playState.hasStream = true;
|
||||
playState.showOffline = false;
|
||||
|
||||
// @ts-ignore
|
||||
window.hasstream = true;
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement?.remove();
|
||||
// @ts-ignore
|
||||
window.playbtnelement?.remove();
|
||||
const playing = sessionStorage.getItem("showedWatchBanner")
|
||||
if (!playing || playing != "true") {
|
||||
if (!showBannerModal.value) showBannerModal.value = true
|
||||
} else {
|
||||
if (!showButtonModal.value) showButtonModal.value = true
|
||||
}
|
||||
|
||||
const playbtn = document.createElement("button");
|
||||
playbtn.style.position = "absolute";
|
||||
playbtn.style.left = "50%";
|
||||
playbtn.style.top = "50%";
|
||||
playbtn.style.transform = "translateX(-50%) translateY(-50%)";
|
||||
playbtn.style.width = "12rem";
|
||||
playbtn.style.height = "6rem";
|
||||
playbtn.style.borderRadius = "1rem";
|
||||
playbtn.style.backgroundColor = "rgb(175, 50, 50)";
|
||||
playbtn.style.color = "black";
|
||||
playbtn.style.fontSize = "1.5em";
|
||||
playbtn.textContent = "< Start >";
|
||||
await handleVideoInput();
|
||||
} else if (mediaStream === null) {
|
||||
console.log("MediaStream is null, Room is offline");
|
||||
playState.showOffline = true
|
||||
playState.hasStream = false;
|
||||
// Clear canvas if it has been set
|
||||
if (canvas.value) {
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
}
|
||||
} else if (playState.video && playState.video.srcObject !== null) {
|
||||
console.log("Setting new mediastream");
|
||||
playState.video.srcObject = mediaStream;
|
||||
playState.hasStream = true;
|
||||
playState.showOffline = true
|
||||
|
||||
playbtn.onclick = () => {
|
||||
playbtn.remove();
|
||||
(video as HTMLVideoElement).play().then(() => {
|
||||
if (canvas.value) {
|
||||
canvas.value.width = (video as HTMLVideoElement).videoWidth;
|
||||
canvas.value.height = (video as HTMLVideoElement).videoHeight;
|
||||
playState.video.play().then(() => {
|
||||
// window.roomOfflineElement?.remove();
|
||||
playState.showOffline = false
|
||||
if (canvas.value && playState.video) {
|
||||
canvas.value.width = playState.video.videoWidth;
|
||||
canvas.value.height = playState.video.videoHeight;
|
||||
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
const renderer = () => {
|
||||
// @ts-ignore
|
||||
if (ctx && window.hasstream) {
|
||||
ctx.drawImage((video as HTMLVideoElement), 0, 0);
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
if (ctx && playState.hasStream && playState.video) {
|
||||
ctx.drawImage(playState.video, 0, 0);
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
}
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
playState.video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
});
|
||||
};
|
||||
document.body.append(playbtn);
|
||||
// @ts-ignore
|
||||
window.playbtnelement = playbtn;
|
||||
} else if (mediaStream === null) {
|
||||
console.log("MediaStream is null, Room is offline");
|
||||
// @ts-ignore
|
||||
window.playbtnelement?.remove();
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement?.remove();
|
||||
// Add a message to the screen
|
||||
const offline = document.createElement("div");
|
||||
offline.style.position = "absolute";
|
||||
offline.style.left = "50%";
|
||||
offline.style.top = "50%";
|
||||
offline.style.transform = "translateX(-50%) translateY(-50%)";
|
||||
offline.style.width = "auto";
|
||||
offline.style.height = "auto";
|
||||
offline.style.color = "lightgray";
|
||||
offline.style.fontSize = "2em";
|
||||
offline.textContent = "Offline";
|
||||
document.body.append(offline);
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement = offline;
|
||||
// @ts-ignore
|
||||
window.hasstream = false;
|
||||
// Clear canvas if it has been set
|
||||
if (canvas.value) {
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
}
|
||||
} else if ((video as HTMLVideoElement).srcObject !== null) {
|
||||
console.log("Setting new mediastream");
|
||||
(video as HTMLVideoElement).srcObject = mediaStream;
|
||||
// @ts-ignore
|
||||
window.hasstream = true;
|
||||
// Start video rendering
|
||||
(video as HTMLVideoElement).play().then(() => {
|
||||
// @ts-ignore
|
||||
window.roomOfflineElement?.remove();
|
||||
if (canvas.value) {
|
||||
canvas.value.width = (video as HTMLVideoElement).videoWidth;
|
||||
canvas.value.height = (video as HTMLVideoElement).videoHeight;
|
||||
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
const renderer = () => {
|
||||
// @ts-ignore
|
||||
if (ctx && window.hasstream) {
|
||||
ctx.drawImage((video as HTMLVideoElement), 0, 0);
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
}
|
||||
}
|
||||
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error handling the media connection", error)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvas}
|
||||
onClick$={async () => {
|
||||
// @ts-ignore
|
||||
if (canvas.value && window.hasstream && !window.nestriLock) {
|
||||
await canvas.value.requestFullscreen()
|
||||
if (document.fullscreenElement !== null) {
|
||||
// @ts-ignore
|
||||
if ('keyboard' in window.navigator && 'lock' in window.navigator.keyboard) {
|
||||
const keys = [
|
||||
"AltLeft",
|
||||
"AltRight",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"ContextMenu",
|
||||
"MetaLeft",
|
||||
"MetaRight"
|
||||
];
|
||||
console.log("requesting keyboard lock");
|
||||
// @ts-ignore
|
||||
window.navigator.keyboard.lock(keys).then(
|
||||
() => {
|
||||
console.log("keyboard lock success");
|
||||
// @ts-ignore
|
||||
window.nestriLock = true;
|
||||
}
|
||||
).catch(
|
||||
(e: any) => {
|
||||
console.log("keyboard lock failed: ", e);
|
||||
// @ts-ignore
|
||||
window.nestriLock = false;
|
||||
}
|
||||
)
|
||||
} else {
|
||||
console.log("keyboard lock not supported, navigator is: ", window.navigator, navigator);
|
||||
// @ts-ignore
|
||||
window.nestriLock = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
|
||||
class="aspect-video h-full w-full object-contain max-h-screen" />
|
||||
<>
|
||||
{playState.showOffline ? (
|
||||
<div class="w-screen h-screen flex justify-center items-center">
|
||||
<span class="text-2xl font-semibold flex items-center gap-2" >
|
||||
Offline
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
< canvas
|
||||
ref={canvas}
|
||||
class="aspect-video h-full w-full object-contain max-h-screen" />
|
||||
{typeof playState.showOffline === "undefined" && (
|
||||
<div class="w-screen h-screen bg-gray-100 dark:bg-gray-900 absolute z-10 flex justify-center items-center">
|
||||
<span class="text-xl font-semibold flex items-center gap-2" >
|
||||
<div data-component="spinner">
|
||||
<div>
|
||||
{new Array(12).fill(0).map((i, k) => (
|
||||
<div key={k} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
Connecting to the relays...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Modal.Root bind:show={showButtonModal} closeOnBackdropClick={false}>
|
||||
<Modal.Panel class="
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[370px] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
|
||||
dark:[box-shadow: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] dark:bg-[#222b]
|
||||
[box-shadow: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] bg-[#fffd]
|
||||
backdrop-blur-lg py-4 px-5 modal" >
|
||||
<div class="size-full flex flex-col">
|
||||
<div class="flex flex-col gap-3" >
|
||||
<button
|
||||
onClick$={async () => {
|
||||
showButtonModal.value = false;
|
||||
await handleVideoInput()
|
||||
await lockPlay();
|
||||
}}
|
||||
class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4" >
|
||||
Continue Watching
|
||||
</button>
|
||||
<button
|
||||
onClick$={() => {
|
||||
nav("/home")
|
||||
}}
|
||||
class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4" >
|
||||
Go Back Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
<Modal.Root bind:show={showBannerModal} closeOnBackdropClick={false}>
|
||||
<Modal.Panel class="
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[37%] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
|
||||
dark:[box-shadow: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] dark:bg-[#222b]
|
||||
[box-shadow: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] bg-[#fffd]
|
||||
backdrop-blur-lg py-4 px-5 modal" >
|
||||
<div class="size-full flex flex-col">
|
||||
<div class="dark:text-white text-black">
|
||||
<h3 class="font-semibold text-2xl tracking-tight mb-2 font-title">Important information from Nestri</h3>
|
||||
<div class="text-sm dark:text-white/[.79] text-[rgba(19,21,23,0.64)]" >
|
||||
This product is in Alpha — please share feedback whenever possible to help us improve. Thanks you for your support! 💖
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:pt-10 sm:block hidden" >
|
||||
<button
|
||||
onClick$={async () => {
|
||||
sessionStorage.setItem("showedWatchBanner", "true");
|
||||
showBannerModal.value = false;
|
||||
await handleVideoInput()
|
||||
await lockPlay();
|
||||
}}
|
||||
class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:hover:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] focus:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] font-medium font-title rounded-lg flex h-[calc(2.25rem+2*1px)] flex-col text-white w-full leading-none truncate bg-primary-500 items-center justify-center" >
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
{/**
|
||||
.spinningCircleInner_b6db20 {
|
||||
transform: rotate(280deg);
|
||||
}
|
||||
.inner_b6db20 {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
contain: paint;
|
||||
} */
|
||||
}
|
||||
|
||||
{/* <div class="loadingPopout_a8c724" role="dialog" tabindex="-1" aria-modal="true"><div class="spinner_b6db20 spinningCircle_b6db20" role="img" aria-label="Loading"><div class="spinningCircleInner_b6db20 inner_b6db20"><svg class="circular_b6db20" viewBox="25 25 50 50"><circle class="path_b6db20 path3_b6db20" cx="50" cy="50" r="20"></circle><circle class="path_b6db20 path2_b6db20" cx="50" cy="50" r="20"></circle><circle class="path_b6db20" cx="50" cy="50" r="20"></circle></svg></div></div></div> */
|
||||
}
|
||||
// .loadingPopout_a8c724 {
|
||||
// background-color: var(--background-secondary);
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// padding: 8px;
|
||||
// }
|
||||
|
||||
// .circular_b6db20 {
|
||||
// animation: spinner-spinning-circle-rotate_b6db20 2s linear infinite;
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
// }
|
||||
|
||||
// 100% {
|
||||
// transform: rotate(360deg);
|
||||
// }
|
||||
|
||||
|
||||
{/* .path3_b6db20 {
|
||||
animation-delay: .23s;
|
||||
stroke: var(--text-brand);
|
||||
}
|
||||
.path_b6db20 {
|
||||
animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite;
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--brand-500);
|
||||
}
|
||||
circle[Attributes Style] {
|
||||
cx: 50;
|
||||
cy: 50;
|
||||
r: 20;
|
||||
}
|
||||
user agent stylesheet
|
||||
:not(svg) {
|
||||
transform-origin: 0px 0px;
|
||||
} */
|
||||
}
|
||||
|
||||
|
||||
// .path2_b6db20 {
|
||||
// animation-delay: .15s;
|
||||
// stroke: var(--text-brand);
|
||||
// opacity: .6;
|
||||
// }
|
||||
// .path_b6db20 {
|
||||
// animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite;
|
||||
// stroke-dasharray: 1, 200;
|
||||
// stroke-dashoffset: 0;
|
||||
// fill: none;
|
||||
// stroke-width: 6;
|
||||
// stroke-miterlimit: 10;
|
||||
// stroke-linecap: round;
|
||||
// stroke: var(--brand-500);
|
||||
// }
|
||||
// circle[Attributes Style] {
|
||||
// cx: 50;
|
||||
// cy: 50;
|
||||
// r: 20;
|
||||
|
||||
|
||||
// function throttle(func, limit) {
|
||||
// let inThrottle;
|
||||
// return function(...args) {
|
||||
// if (!inThrottle) {
|
||||
// func.apply(this, args);
|
||||
// inThrottle = true;
|
||||
// setTimeout(() => inThrottle = false, limit);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Use it like this:
|
||||
// const throttledMouseMove = throttle((x, y) => {
|
||||
// websocket.send(JSON.stringify({
|
||||
// type: 'mousemove',
|
||||
// x: x,
|
||||
// y: y
|
||||
// }));
|
||||
// }, 16); // ~60fps
|
||||
|
||||
// use std::time::Instant;
|
||||
|
||||
// // Add these to your AppState
|
||||
// struct AppState {
|
||||
// pipeline: Arc<Mutex<gst::Pipeline>>,
|
||||
// last_mouse_move: Arc<Mutex<(i32, i32, Instant)>>, // Add this
|
||||
// }
|
||||
|
||||
// // Then in your MouseMove handler:
|
||||
// InputMessage::MouseMove { x, y } => {
|
||||
// let mut last_move = state.last_mouse_move.lock().unwrap();
|
||||
// let now = Instant::now();
|
||||
|
||||
// // Only process if coordinates are different or enough time has passed
|
||||
// if (last_move.0 != x || last_move.1 != y) &&
|
||||
// (now.duration_since(last_move.2).as_millis() > 16) { // ~60fps
|
||||
|
||||
// println!("Mouse moved to x: {}, y: {}", x, y);
|
||||
|
||||
// let structure = gst::Structure::builder("MouseMoveRelative")
|
||||
// .field("pointer_x", x as f64)
|
||||
// .field("pointer_y", y as f64)
|
||||
// .build();
|
||||
|
||||
// let event = gst::event::CustomUpstream::new(structure);
|
||||
// pipeline.send_event(event);
|
||||
|
||||
// // Update last position and time
|
||||
// *last_move = (x, y, now);
|
||||
// }
|
||||
// }
|
||||
})
|
||||
57
apps/www/sst-env.d.ts
vendored
@@ -2,57 +2,8 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"Auth": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"AuthFingerprintKey": {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"CloudflareAuthKV": {
|
||||
"type": "sst.cloudflare.Kv"
|
||||
}
|
||||
"DiscordClientID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"DiscordClientSecret": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GithubClientID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GithubClientSecret": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAdminToken": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAppId": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"LoopsApiKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
export {}
|
||||
@@ -9,7 +9,8 @@ import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { qwikReact } from "@builder.io/qwik-react/vite";
|
||||
import pkg from "./package.json";
|
||||
import { partytownVite } from "@builder.io/partytown/utils";
|
||||
import { join } from "path";
|
||||
import { join, resolve } from "path";
|
||||
|
||||
type PkgDep = Record<string, string>;
|
||||
const { dependencies = {}, devDependencies = {} } = pkg as any as {
|
||||
dependencies: PkgDep;
|
||||
@@ -30,6 +31,11 @@ export default defineConfig((): UserConfig => {
|
||||
qwikReact(),
|
||||
partytownVite({ dest: join(__dirname, "dist", "~partytown") }),
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '~', replacement: resolve(__dirname,"public") },
|
||||
],
|
||||
},
|
||||
// This tells Vite which dependencies to pre-build in dev mode.
|
||||
optimizeDeps: {
|
||||
// Put problematic deps that break bundling here, mostly those with binaries.
|
||||
|
||||