feat: Connect the frontend to the API (#160)

This commit is contained in:
Wanjohi
2025-01-18 07:12:47 +03:00
committed by GitHub
parent dfe37a6cec
commit f480ced756
56 changed files with 2109 additions and 743 deletions

View File

@@ -34,11 +34,14 @@
"@builder.io/qwik": "^1.8.0",
"@builder.io/qwik-city": "^1.8.0",
"@builder.io/qwik-react": "0.5.0",
"@modular-forms/qwik": "^0.27.0",
"@modular-forms/qwik": "^0.29.0",
"@nestri/input": "*",
"@nestri/libmoq": "*",
"@nestri/sdk": "0.1.0-alpha.11",
"@nestri/ui": "*",
"@openauthjs/openauth": "^0.2.6",
"@polar-sh/checkout": "^0.1.8",
"@polar-sh/sdk": "^0.21.1",
"@qwik-ui/headless": "^0.6.4",
"@types/eslint": "8.56.10",
"@types/howler": "^2.2.12",

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,9 +0,0 @@
import { component$ } from "@builder.io/qwik"
export default component$(() => {
return (
<div class="font-title">
Device
</div>
)
})

View File

@@ -1,54 +0,0 @@
import { $, component$, useVisibleTask$ } from "@builder.io/qwik";
import { createClient } from "@openauthjs/openauth/client";
function getHashParams(url: URL) {
const urlString = url.toString()
const hash = urlString.substring(urlString.indexOf('#') + 1); // Extract the part after the #
console.log("url", hash)
const params = new URLSearchParams(hash);
const paramsObj = {} as any;
for (const [key, value] of params.entries()) {
paramsObj[key] = decodeURIComponent(value);
}
console.log(paramsObj)
return paramsObj;
}
function removeURLParams() {
const newURL = window.location.origin + window.location.pathname; // Just origin and path
window.location.replace(newURL);
}
export default component$(() => {
const login = $(async () => {
const client = createClient({
clientID: "www",
issuer: "https://auth.lauryn.dev.nestri.io"
})
const { url } = await client.authorize("http://localhost:5173/login-test", "token", { pkce: true })
window.location.href = url
})
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(async () => {
const urlObj = new URL(window.location.href);
const params = getHashParams(urlObj)
if (params.access_token && params.refresh_token) {
localStorage.setItem("access_token", params.access_token)
localStorage.setItem("refresh_token", params.refresh_token)
removeURLParams()
}
})
return (
<div class="h-screen w-screen flex justify-center items-center">
<button class="px-2 py-1 font-title text-lg bg-gray-400 rounded-lg" onClick$={login}>
Login
</button>
</div>
)
})

View File

@@ -1,20 +0,0 @@
import { component$ } from "@builder.io/qwik";
export default component$(() => {
return (
<>
<div class="w-full justify-center py-20 gap-10 flex flex-col px-6" >
<div class="mx-auto w-full items-center max-w-xl flex gap-3 flex-col">
<span class="text-base underline underline-offset-4 px-2">Step 2 of 2</span>
<h1 class="text-4xl font-bold font-title">
You're almost done
</h1>
<p class="text-lg font-medium text-gray-600/70 dark:text-gray-400/70">
Please follow the steps to configure your game and install it
</p>
</div>
</div>
</>
)
})

View File

@@ -1,79 +0,0 @@
import { component$ } from "@builder.io/qwik";
const games = [
{
cover: 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/444200/library_600x900_2x.jpg',
release_date: 1478710740000,
compatibility: 'playable',
name: 'World of Tanks Blitz',
appid: 444200
},
{
cover: 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1085660/library_600x900_2x.jpg',
release_date: 1569949200000,
compatibility: 'unsupported',
name: 'Destiny 2',
appid: 1085660
},
{
cover: 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1172470/library_600x900_2x.jpg',
release_date: 1604548800000,
compatibility: 'playable',
name: 'Apex Legends',
appid: 1172470
},
{
cover: 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1229380/library_600x900_2x.jpg',
release_date: 1614870001000,
compatibility: 'perfect',
name: 'Everhood',
appid: 1229380
},
{
cover: 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1637320/library_600x900_2x.jpg',
release_date: 1664296270000,
compatibility: 'perfect',
name: 'Dome Keeper',
appid: 1637320
},
{
cover: 'https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2581970/library_600x900_2x.jpg',
release_date: 1698246127000,
compatibility: 'playable',
name: 'Shell Runner - Prelude',
appid: 2581970
}
]
export default component$(() => {
return (
<>
<div class="w-full justify-center py-20 gap-10 flex flex-col px-6" >
<div class="mx-auto w-full items-center max-w-xl flex gap-3 flex-col">
<span class="text-base underline underline-offset-4 px-2">Step 1 of 2</span>
<h1 class="text-4xl font-bold font-title ">
Let's play something cool
</h1>
<p class="text-lg font-medium text-gray-600/70 dark:text-gray-400/70">
Choose a game to play from your Steam library
</p>
</div>
<ul class="w-full max-w-xl mx-auto grid gap-2 grid-cols-2 sm:grid-cols-2 md:grid-cols-3" >
{games.map((game) => (
<li key={game.appid}>
<div class="rounded-2xl cursor-pointer ring-2 hover:ring-primary-500 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-black overflow-hidden group gap-3 transition-all duration-200 p-5 flex-col aspect-[14/12] relative">
<div class="absolute inset-0 p-0">
<img height={133} width={95} src={game.cover} alt={game.name} class="object-top h-full w-full object-cover rounded-lg blur-xl opacity-20" />
</div>
<h2 class="relative font-semibold">{game.name}</h2>
<div class="absolute transition-all duration-100 -bottom-16 rotate-6 left-1/2 -translate-x-1/2 w-1/2 flex items-center justify-center aspect-[10/14] rounded-sm group-hover:rotate-[5deg] group-hover:shadow-lg group-hover:shadow-gray-400 dark:group-hover:shadow-gray-600 group-hover:scale-105">
<img height={133} width={95} src={`https://nexus.nestri.workers.dev/image/cover/${game.appid}.avif?width=400`} alt="Logo" class="pointer-events-none object-top h-full object-cover w-full rounded-lg aspect-[10/14]" />
</div>
</div>
</li>
))}
</ul>
</div>
</>
)
})

View File

@@ -1,217 +0,0 @@
// import { auth } from "@nestri/ui";
import { Button } from "@nestri/ui/react"
import { buttonVariants } from "@nestri/ui/design";
// import { type Provider } from "@supabase/supabase-js";
import { Link } from "@builder.io/qwik-city";
import { $, component$, useSignal } from "@builder.io/qwik";
// type AuthFlowProps = {
// flow: string
// }
const flow: any = "join"
export default component$(() => {
const isLoading = useSignal(false);
const isGHLoading = useSignal(false);
const setIsLoading = $((v: boolean) => isLoading.value = v)
const setIsGHLoading = $((v: boolean) => isGHLoading.value = v)
// const location = useLocation();
// const authenticateUser = $(async (provider: any) => {
// await auth.openWindow(`${location.url.origin}/api/auth/${provider}`)
// })
return (
<>
<div class='h-screen w-full flex overflow-hidden justify-center items-center' >
<div
style={{ animationDelay: "0.05s", animationFillMode: "forwards" }}
class='animate-fade-up flex flex-col px-5 max-[479px]:px-6 max-w-[380px] gap-2 w-full' >
{/* <img src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} class="w-[70px] md:w-[80px] aspect-[90/69]" /> */}
<h1 class='font-semibold text-2xl font-title leading-6' >
{flow == "login" ? "Login" : "Join"}
</h1>
{flow == "join" ? (
<p class='max-w-[400px] font-normal text-sm dark:text-primary-50/70 text-primary-950/70'>
Already have an account?
<Link
href="/login"
class={buttonVariants.link()}
>Login
</Link>
</p>
) : (
<p class='max-w-[400px] font-normal text-sm dark:text-primary-50/70 text-primary-950/70'>
Don't have an account yet?
<Link
href="/join"
class={buttonVariants.link()}
>Join
</Link>
</p>
)}
<hr class='my-2 border-none h-0.5 bg-primary-950/60 dark:bg-primary-50/60' />
{/* <Button.Root
disabled={isGHLoading.value || isLoading.value}
isLoading={isLoading.value}
setIsLoading={setIsLoading}
loadingTime={2000}
client:load
intent="primary"
// onClick$={async () => await authenticateUser("discord")}
size="md"
class="w-full gap-4 from-[#5865F2] to-[#4445e7] [--btn-border-color:#3836cc] dark:border-[#8093f9]/75">
<Button.Icon
isLoading={isLoading.value}
client:load>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M20.317 4.492c-1.53-.69-3.17-1.2-4.885-1.49a.075.075 0 0 0-.079.036c-.21.369-.444.85-.608 1.23a18.566 18.566 0 0 0-5.487 0a12.36 12.36 0 0 0-.617-1.23A.077.077 0 0 0 8.562 3c-1.714.29-3.354.8-4.885 1.491a.07.07 0 0 0-.032.027C.533 9.093-.32 13.555.099 17.961a.08.08 0 0 0 .031.055a20.03 20.03 0 0 0 5.993 2.98a.078.078 0 0 0 .084-.026a13.83 13.83 0 0 0 1.226-1.963a.074.074 0 0 0-.041-.104a13.201 13.201 0 0 1-1.872-.878a.075.075 0 0 1-.008-.125c.126-.093.252-.19.372-.287a.075.075 0 0 1 .078-.01c3.927 1.764 8.18 1.764 12.061 0a.075.075 0 0 1 .079.009c.12.098.245.195.372.288a.075.075 0 0 1-.006.125c-.598.344-1.22.635-1.873.877a.075.075 0 0 0-.041.105c.36.687.772 1.341 1.225 1.962a.077.077 0 0 0 .084.028a19.963 19.963 0 0 0 6.002-2.981a.076.076 0 0 0 .032-.054c.5-5.094-.838-9.52-3.549-13.442a.06.06 0 0 0-.031-.028M8.02 15.278c-1.182 0-2.157-1.069-2.157-2.38c0-1.312.956-2.38 2.157-2.38c1.21 0 2.176 1.077 2.157 2.38c0 1.312-.956 2.38-2.157 2.38m7.975 0c-1.183 0-2.157-1.069-2.157-2.38c0-1.312.955-2.38 2.157-2.38c1.21 0 2.176 1.077 2.157 2.38c0 1.312-.946 2.38-2.157 2.38" />
</svg>
</Button.Icon>
<Button.Label
isLoading={isLoading.value}>
Continue with Discord
</Button.Label>
</Button.Root> */}
<Button.Root
disabled={isLoading.value || isGHLoading.value}
isLoading={isLoading.value}
setIsLoading={setIsLoading}
client:load
loadingTime={5000}
// onClick$={handleSteamLogin}
intent="primary"
size="md"
class="w-full gap-4 from-[#2a475e] to-[#2a475e]/80 [--btn-border-color:#1b2838] dark:border-[#1b2838]/75">
<Button.Icon
isLoading={isLoading.value}
client:load>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
<g fill="currentColor">
<path d="M.329 10.333A8.01 8.01 0 0 0 7.99 16C12.414 16 16 12.418 16 8s-3.586-8-8.009-8A8.006 8.006 0 0 0 0 7.468l.003.006l4.304 1.769A2.2 2.2 0 0 1 5.62 8.88l1.96-2.844l-.001-.04a3.046 3.046 0 0 1 3.042-3.043a3.046 3.046 0 0 1 3.042 3.043a3.047 3.047 0 0 1-3.111 3.044l-2.804 2a2.223 2.223 0 0 1-3.075 2.11a2.22 2.22 0 0 1-1.312-1.568L.33 10.333Z" /><path d="M4.868 12.683a1.715 1.715 0 0 0 1.318-3.165a1.7 1.7 0 0 0-1.263-.02l1.023.424a1.261 1.261 0 1 1-.97 2.33l-.99-.41a1.7 1.7 0 0 0 .882.84Zm3.726-6.687a2.03 2.03 0 0 0 2.027 2.029a2.03 2.03 0 0 0 2.027-2.029a2.03 2.03 0 0 0-2.027-2.027a2.03 2.03 0 0 0-2.027 2.027m2.03-1.527a1.524 1.524 0 1 1-.002 3.048a1.524 1.524 0 0 1 .002-3.048" />
</g>
</svg>
</Button.Icon>
<Button.Label
loadingText="Linking Steam account..."
class="text-ellipsis whitespace-nowrap"
isLoading={isLoading.value}>
Link your Steam account
</Button.Label>
<div class="w-[8%]" />
</Button.Root>
<Button.Root
// disabled={isGHLoading.value || isLoading.value}
disabled
isLoading={isGHLoading.value}
setIsLoading={setIsGHLoading}
client:load
// loadingTime={2000}
intent="primary"
size="md"
class="w-full gap-4 mt-2 from-[#434D56] to-[#24292e] [--btn-border-color:#1B1F22] dark:border-[#63707E]/75">
<Button.Icon
isLoading={isGHLoading.value}
client:load>
<svg xmlns="http://www.w3.org/2000/svg" class="fill-gray-500" width="24" height="24" viewBox="0 0 815 815">
<path d="M375.46 252.45v-75.97c0-12.08-5.58-17.69-17.18-17.69h-18.9v111.36h18.9c11.6 0 17.18-5.61 17.18-17.7zm-60.68 270.86l-10.32 25.99h20.51zM685.37 13.5H129.63c-45.04 0-61.67 16.62-61.67 61.69v543.78c0 5.1.21 9.84.66 14.23 1.03 9.84 1.22 19.37 10.37 30.22.89 1.06 10.23 8.01 10.23 8.01 5.02 2.46 8.46 4.28 14.12 6.56l273.65 114.65c14.21 6.51 20.15 9.05 30.46 8.85h0 .04.04 0c10.32.2 16.26-2.33 30.47-8.85l273.65-114.65c5.67-2.28 9.1-4.09 14.12-6.56 0 0 9.34-6.95 10.23-8.01 9.15-10.85 9.34-20.39 10.37-30.22.45-4.39.66-9.13.66-14.23V75.19c.01-45.07-16.62-61.69-61.66-61.69zM442.15 118.22h45.97v302.13h-45.97V118.22zm3.11 375.7h26.77v91.42h-25.21v-52.5l-23.38 35.79h-.52l-23.25-35.52v52.24h-24.82v-91.42h26.77l21.81 35.39 21.83-35.4zm-151.84-375.7h72.16c37.37 0 55.84 18.56 55.84 56.11v80.28c0 37.55-18.47 56.1-55.84 56.1h-26.2v109.63h-45.96V118.22zm-125.27 0h102.23v41.86H214.1v85.46h54.12v41.86H214.1v91.08h57.13v41.86H168.15V118.22zm87.86 454.85c-9.67 7.97-23.12 14.11-39.7 14.11-28.47 0-49.76-19.59-49.76-47.28v-.26c0-26.64 20.9-47.54 49.24-47.54 16.06 0 27.43 4.96 37.09 13.32l-14.89 17.89c-6.53-5.49-13.06-8.62-22.07-8.62-13.19 0-23.38 11.1-23.38 25.08v.26c0 14.76 10.32 25.34 24.82 25.34 6.14 0 10.84-1.31 14.63-3.79v-11.1h-18.02v-18.55h42.06v41.14zm46.76-79.8h24.42l38.92 92.08h-27.17l-6.66-16.33h-35.26l-6.53 16.33h-26.64l38.92-92.08zm102.61 244.55l-128.81-44.28h263.1l-134.29 44.28zm157.56-152.47h-74.18v-91.42h73.53v21.55h-48.46v13.84h43.88v19.98h-43.88v14.5h49.11v21.55zm-50.53-218.52V171.74c0-37.56 18.48-56.11 55.84-56.11h22.34c37.36 0 55.4 18.13 55.4 55.68v61.73h-45.1V173.9c0-12.09-5.59-17.69-17.18-17.69h-7.73c-12.03 0-17.61 5.61-17.61 17.69v190.78c0 12.09 5.59 17.69 17.61 17.69h8.6c11.59 0 17.18-5.61 17.18-17.69v-68.2h45.1v70.35c0 37.56-18.47 56.11-55.84 56.11h-22.77c-37.37 0-55.84-18.56-55.84-56.11zm136.86 190.3c0 18.68-14.76 29.78-36.96 29.78-16.19 0-31.61-5.09-42.84-15.15l14.11-16.85c9.01 7.18 19.07 10.97 29.65 10.97 6.79 0 10.45-2.35 10.45-6.27v-.26c0-3.79-3-5.88-15.41-8.75-19.46-4.44-34.48-9.93-34.48-28.73v-.26c0-16.98 13.45-29.26 35.39-29.26 15.54 0 27.69 4.18 37.61 12.15l-12.67 17.89c-8.36-5.88-17.5-9.01-25.6-9.01-6.14 0-9.14 2.61-9.14 5.88v.26c0 4.18 3.14 6.01 15.8 8.88 21.03 4.57 34.09 11.36 34.09 28.47v.26z" />
</svg>
</Button.Icon>
<Button.Label
class="text-ellipsis whitespace-nowrap"
isLoading={isGHLoading.value}>
Link your Epic Games account
</Button.Label>
</Button.Root>
<Button.Root
// disabled={isGHLoading.value || isLoading.value}
disabled
isLoading={isGHLoading.value}
setIsLoading={setIsGHLoading}
client:load
// loadingTime={2000}
intent="primary"
size="md"
class="w-full gap-4 mt-2 from-[rgb(110,29,114)] to-[rgb(164,41,171)] [--btn-border-color:rgb(110,29,114)] dark:border-[#63707E]/75">
<Button.Icon
isLoading={isGHLoading.value}
client:load>
<svg xmlns="http://www.w3.org/2000/svg" class="text-gray-500" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 57 69" version="1.1">
<defs>
<polygon id="path-1" points="0 0.012 56.539 0.012 56.539 53.814 0 53.814" />
<polygon id="path-3" points="0 0.6554 7.1358 0.6554 7.1358 9 0 9" />
<polygon id="path-5" points="0.3397 0.6554 7.4757 0.6554 7.4757 9 0.3397 9" />
<polygon id="path-7" points="0.0474 0.7472 8.539 0.7472 8.539 8.9082 0.0474 8.9082" />
</defs>
<g id="LOGO-/-GOG.COM-/-VERTICAL" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-3" transform="translate(0.000000, -0.011500)">
<mask id="mask-2" fill="currentColor">
<use xlink:href="#path-1" />
</mask>
<g id="Clip-2" />
<path d="M50.335,25.837 C50.335,27.203 49.231,28.3 47.874,28.3 L37.316,28.3 L37.316,25.128 L46.212,25.128 L46.218,25.128 C46.743,25.128 47.165,24.7 47.165,24.178 L47.165,24.172 L47.165,13.698 L47.165,13.69 C47.165,13.167 46.743,12.743 46.218,12.743 L46.212,12.743 L41.444,12.743 L41.435,12.743 C40.917,12.743 40.494,13.167 40.494,13.69 L40.494,13.698 L40.494,18.457 L40.494,18.463 C40.494,18.987 40.917,19.412 41.435,19.412 L41.444,19.412 L45.254,19.412 L45.254,22.585 L39.78,22.585 C38.418,22.585 37.316,21.485 37.316,20.125 L37.316,12.03 C37.316,10.67 38.418,9.568 39.78,9.568 L47.874,9.568 C49.231,9.568 50.335,10.67 50.335,12.03 L50.335,25.837 Z M50.326,44.25 L47.198,44.25 L47.198,34.552 L45.011,34.552 L45,34.552 C44.482,34.552 44.069,34.97 44.069,35.486 L44.069,35.492 L44.069,44.25 L40.94,44.25 L40.94,34.552 L38.749,34.552 L38.742,34.552 C38.225,34.552 37.811,34.97 37.811,35.486 L37.811,35.492 L37.811,44.258 L34.684,44.258 L34.684,33.845 C34.684,32.512 35.767,31.426 37.109,31.426 L50.326,31.426 L50.326,44.25 Z M32.318,22.585 L24.22,22.585 C22.86,22.585 21.761,21.485 21.761,20.125 L21.761,12.03 C21.761,10.67 22.86,9.568 24.22,9.568 L32.318,9.568 C33.675,9.568 34.778,10.67 34.778,12.03 L34.778,20.125 C34.778,21.485 33.675,22.585 32.318,22.585 Z M32.178,41.824 C32.178,43.164 31.092,44.25 29.754,44.25 L21.777,44.25 C20.435,44.25 19.351,43.164 19.351,41.824 L19.351,33.845 C19.351,32.512 20.435,31.426 21.777,31.426 L29.754,31.426 C31.092,31.426 32.178,32.512 32.178,33.845 L32.178,41.824 Z M19.221,25.837 C19.221,27.203 18.118,28.3 16.759,28.3 L6.203,28.3 L6.203,25.128 L15.093,25.128 L15.103,25.128 C15.627,25.128 16.047,24.7 16.047,24.178 L16.047,24.172 L16.047,13.698 L16.047,13.69 C16.047,13.167 15.627,12.743 15.103,12.743 L15.093,12.743 L10.33,12.743 L10.319,12.743 C9.796,12.743 9.376,13.167 9.376,13.69 L9.376,13.698 L9.376,18.457 L9.376,18.463 C9.376,18.987 9.796,19.412 10.319,19.412 L10.33,19.412 L14.141,19.412 L14.141,22.585 L8.664,22.585 C7.302,22.585 6.203,21.485 6.203,20.125 L6.203,12.03 C6.203,10.67 7.302,9.568 8.664,9.568 L16.759,9.568 C18.118,9.568 19.221,10.67 19.221,12.03 L19.221,25.837 Z M16.847,34.552 L10.28,34.552 L10.27,34.552 C9.754,34.552 9.34,34.97 9.34,35.486 C9.34,35.486 9.342,35.486 9.342,35.492 L9.34,35.492 L9.34,40.186 L9.342,40.186 L9.34,40.19 C9.34,40.704 9.754,41.126 10.27,41.126 L10.28,41.126 L10.96,41.126 L16.847,41.126 L16.847,44.258 L8.64,44.258 L8.64,44.25 C7.296,44.25 6.21,43.164 6.21,41.824 L6.21,33.845 C6.21,32.512 7.296,31.426 8.64,31.426 L16.847,31.426 L16.847,34.552 Z M52.951,0.011 L3.587,0.011 C1.606,0.011 0,1.617 0,3.597 L0,50.228 C0,52.208 1.606,53.814 3.587,53.814 L52.951,53.814 C54.933,53.814 56.539,52.208 56.539,50.228 L56.539,3.597 C56.539,1.617 54.933,0.011 52.951,0.011 Z" id="Fill-1" fill="currentColor" mask="url(#mask-2)" />
</g>
<path d="M30.6612,12.7318 C31.1862,12.7318 31.5972,13.1558 31.5972,13.6788 L31.5972,13.6868 L31.5972,18.4458 L31.5972,18.4518 C31.5972,18.9758 31.1862,19.4008 30.6612,19.4008 L30.6502,19.4008 L25.8872,19.4008 L25.8802,19.4008 C25.3552,19.4008 24.9342,18.9758 24.9342,18.4518 L24.9342,18.4458 L24.9342,13.6868 L24.9342,13.6788 C24.9342,13.1558 25.3552,12.7318 25.8802,12.7318 L25.8872,12.7318 L30.6502,12.7318 L30.6612,12.7318" id="Fill-4" fill="currentColor" />
<path d="M28.1226,34.5404 C28.6366,34.5404 29.0506,34.9584 29.0506,35.4754 L29.0506,35.4804 L29.0506,40.1744 C29.0506,40.6894 28.6366,41.1104 28.1226,41.1104 C28.1176,41.1104 28.1166,41.1054 28.1116,41.1054 L28.1116,41.1104 L23.4196,41.1104 L23.4196,41.1054 C23.4196,41.1054 23.4146,41.1104 23.4096,41.1104 C22.8936,41.1104 22.4796,40.6894 22.4796,40.1744 L22.4796,35.4804 L22.4796,35.4754 C22.4796,34.9584 22.8936,34.5404 23.4096,34.5404 L23.4196,34.5404 L28.1116,34.5404 L28.1226,34.5404" id="Fill-6" fill="currentColor" />
<g id="Group-10" transform="translate(0.000000, 59.988500)">
<mask id="mask-4" fill="currentColor">
<use xlink:href="#path-3" />
</mask>
<g id="Clip-9" />
<path d="M5.8238,8.7804 C5.3588,8.9264 4.8488,9.0004 4.2948,9.0004 C3.6368,9.0004 3.0448,8.8984 2.5178,8.6944 C1.9898,8.4914 1.5388,8.2074 1.1648,7.8414 C0.7908,7.4734 0.5028,7.0354 0.3018,6.5224 C0.1008,6.0094 -0.0002,5.4454 -0.0002,4.8314 C-0.0002,4.2084 0.0968,3.6404 0.2938,3.1284 C0.4888,2.6144 0.7668,2.1764 1.1278,1.8084 C1.4888,1.4424 1.9268,1.1594 2.4438,0.9584 C2.9598,0.7564 3.5368,0.6554 4.1748,0.6554 C4.4978,0.6554 4.7978,0.6794 5.0748,0.7264 C5.3518,0.7744 5.6088,0.8424 5.8438,0.9314 C6.0788,1.0214 6.2978,1.1304 6.4988,1.2564 C6.6998,1.3844 6.8878,1.5274 7.0628,1.6864 L6.7488,2.1874 C6.6998,2.2634 6.6358,2.3124 6.5578,2.3334 C6.4808,2.3544 6.3938,2.3364 6.2988,2.2794 C6.2078,2.2254 6.1038,2.1614 5.9858,2.0854 C5.8678,2.0094 5.7248,1.9364 5.5558,1.8664 C5.3868,1.7964 5.1878,1.7354 4.9578,1.6864 C4.7278,1.6364 4.4558,1.6134 4.1408,1.6134 C3.6808,1.6134 3.2658,1.6874 2.8928,1.8374 C2.5208,1.9874 2.2038,2.2014 1.9418,2.4804 C1.6798,2.7594 1.4788,3.0974 1.3378,3.4944 C1.1978,3.8914 1.1278,4.3364 1.1278,4.8314 C1.1278,5.3424 1.2008,5.8014 1.3468,6.2064 C1.4928,6.6094 1.7018,6.9544 1.9738,7.2364 C2.2448,7.5204 2.5738,7.7354 2.9618,7.8844 C3.3488,8.0314 3.7828,8.1054 4.2658,8.1054 C4.4558,8.1054 4.6338,8.0934 4.7988,8.0714 C4.9638,8.0494 5.1218,8.0174 5.2738,7.9774 C5.4258,7.9364 5.5728,7.8874 5.7128,7.8294 C5.8528,7.7704 5.9958,7.7034 6.1398,7.6274 L6.1398,5.8274 L4.8698,5.8274 C4.7978,5.8274 4.7398,5.8054 4.6968,5.7644 C4.6528,5.7224 4.6308,5.6724 4.6308,5.6104 L4.6308,4.9844 L7.1358,4.9844 L7.1358,8.1174 C6.7268,8.4134 6.2888,8.6344 5.8238,8.7804" id="Fill-8" fill="currentColor" mask="url(#mask-4)" />
</g>
<path d="M15.6856,64.8197 C15.6856,64.3177 15.6176,63.8677 15.4806,63.4687 C15.3446,63.0707 15.1506,62.7337 14.8996,62.4587 C14.6486,62.1827 14.3456,61.9707 13.9886,61.8237 C13.6316,61.6747 13.2326,61.6017 12.7926,61.6017 C12.3556,61.6017 11.9586,61.6747 11.6026,61.8237 C11.2456,61.9707 10.9406,62.1827 10.6876,62.4587 C10.4356,62.7337 10.2406,63.0707 10.1036,63.4687 C9.9676,63.8677 9.8986,64.3177 9.8986,64.8197 C9.8986,65.3207 9.9676,65.7697 10.1036,66.1667 C10.2406,66.5627 10.4356,66.8987 10.6876,67.1737 C10.9406,67.4497 11.2456,67.6607 11.6026,67.8057 C11.9586,67.9527 12.3556,68.0257 12.7926,68.0257 C13.2326,68.0257 13.6316,67.9527 13.9886,67.8057 C14.3456,67.6607 14.6486,67.4497 14.8996,67.1737 C15.1506,66.8987 15.3446,66.5627 15.4806,66.1667 C15.6176,65.7697 15.6856,65.3207 15.6856,64.8197 M16.8186,64.8197 C16.8186,65.4297 16.7226,65.9917 16.5286,66.5027 C16.3356,67.0117 16.0616,67.4527 15.7086,67.8197 C15.3556,68.1887 14.9316,68.4747 14.4356,68.6777 C13.9406,68.8817 13.3926,68.9827 12.7926,68.9827 C12.1926,68.9827 11.6456,68.8817 11.1516,68.6777 C10.6586,68.4747 10.2356,68.1887 9.8816,67.8197 C9.5286,67.4527 9.2556,67.0117 9.0616,66.5027 C8.8686,65.9917 8.7716,65.4297 8.7716,64.8197 C8.7716,64.2077 8.8686,63.6467 9.0616,63.1347 C9.2556,62.6257 9.5286,62.1847 9.8816,61.8147 C10.2356,61.4447 10.6586,61.1567 11.1516,60.9517 C11.6456,60.7467 12.1926,60.6437 12.7926,60.6437 C13.3926,60.6437 13.9406,60.7467 14.4356,60.9517 C14.9316,61.1567 15.3556,61.4447 15.7086,61.8147 C16.0616,62.1847 16.3356,62.6257 16.5286,63.1347 C16.7226,63.6467 16.8186,64.2077 16.8186,64.8197" id="Fill-11" fill="currentColor" />
<g id="Group-15" transform="translate(18.000000, 59.988500)">
<mask id="mask-6" fill="currentColor">
<use xlink:href="#path-5" />
</mask>
<g id="Clip-14" />
<path d="M6.1637,8.7804 C5.6987,8.9264 5.1887,9.0004 4.6337,9.0004 C3.9767,9.0004 3.3847,8.8984 2.8567,8.6944 C2.3297,8.4914 1.8777,8.2074 1.5047,7.8414 C1.1307,7.4734 0.8427,7.0354 0.6417,6.5224 C0.4397,6.0094 0.3397,5.4454 0.3397,4.8314 C0.3397,4.2084 0.4367,3.6404 0.6327,3.1284 C0.8287,2.6144 1.1067,2.1764 1.4677,1.8084 C1.8277,1.4424 2.2667,1.1594 2.7827,0.9584 C3.2997,0.7564 3.8767,0.6554 4.5147,0.6554 C4.8377,0.6554 5.1377,0.6794 5.4147,0.7264 C5.6917,0.7744 5.9477,0.8424 6.1837,0.9314 C6.4187,1.0214 6.6377,1.1304 6.8377,1.2564 C7.0397,1.3844 7.2267,1.5274 7.4017,1.6864 L7.0887,2.1874 C7.0397,2.2634 6.9757,2.3124 6.8977,2.3334 C6.8197,2.3544 6.7337,2.3364 6.6387,2.2794 C6.5477,2.2254 6.4437,2.1614 6.3257,2.0854 C6.2077,2.0094 6.0647,1.9364 5.8957,1.8664 C5.7267,1.7964 5.5267,1.7354 5.2977,1.6864 C5.0677,1.6364 4.7947,1.6134 4.4797,1.6134 C4.0207,1.6134 3.6047,1.6874 3.2327,1.8374 C2.8607,1.9874 2.5437,2.2014 2.2817,2.4804 C2.0197,2.7594 1.8187,3.0974 1.6777,3.4944 C1.5377,3.8914 1.4677,4.3364 1.4677,4.8314 C1.4677,5.3424 1.5397,5.8014 1.6857,6.2064 C1.8327,6.6094 2.0417,6.9544 2.3127,7.2364 C2.5847,7.5204 2.9137,7.7354 3.3017,7.8844 C3.6887,8.0314 4.1227,8.1054 4.6057,8.1054 C4.7947,8.1054 4.9727,8.0934 5.1387,8.0714 C5.3037,8.0494 5.4617,8.0174 5.6137,7.9774 C5.7657,7.9364 5.9117,7.8874 6.0527,7.8294 C6.1927,7.7704 6.3347,7.7034 6.4797,7.6274 L6.4797,5.8274 L5.2087,5.8274 C5.1377,5.8274 5.0797,5.8054 5.0357,5.7644 C4.9927,5.7224 4.9697,5.6724 4.9697,5.6104 L4.9697,4.9844 L7.4757,4.9844 L7.4757,8.1174 C7.0657,8.4134 6.6287,8.6344 6.1637,8.7804" id="Fill-13" fill="currentColor" mask="url(#mask-6)" />
</g>
<path d="M27.3995,68.2699 C27.3995,68.1719 27.4175,68.0799 27.4535,67.9919 C27.4895,67.9049 27.5385,67.8289 27.6015,67.7639 C27.6645,67.6999 27.7385,67.6489 27.8265,67.6099 C27.9135,67.5719 28.0065,67.5529 28.1055,67.5529 C28.2045,67.5529 28.2965,67.5719 28.3845,67.6099 C28.4715,67.6489 28.5475,67.6999 28.6125,67.7639 C28.6765,67.8289 28.7285,67.9049 28.7665,67.9919 C28.8035,68.0799 28.8225,68.1719 28.8225,68.2699 C28.8225,68.3729 28.8035,68.4669 28.7665,68.5519 C28.7285,68.6379 28.6765,68.7129 28.6125,68.7779 C28.5475,68.8419 28.4715,68.8919 28.3845,68.9279 C28.2965,68.9639 28.2045,68.9829 28.1055,68.9829 C28.0065,68.9829 27.9135,68.9639 27.8265,68.9279 C27.7385,68.8919 27.6645,68.8419 27.6015,68.7779 C27.5385,68.7129 27.4895,68.6379 27.4535,68.5519 C27.4175,68.4669 27.3995,68.3729 27.3995,68.2699" id="Fill-16" fill="currentColor" />
<path d="M36.4151,67.2113 C36.4771,67.2113 36.5291,67.2353 36.5741,67.2853 L37.0131,67.7583 C36.6791,68.1463 36.2731,68.4463 35.7971,68.6643 C35.3211,68.8803 34.7451,68.9883 34.0681,68.9883 C33.4831,68.9883 32.9521,68.8873 32.4741,68.6833 C31.9951,68.4803 31.5871,68.1953 31.2501,67.8293 C30.9111,67.4623 30.6491,67.0243 30.4641,66.5113 C30.2771,65.9973 30.1851,65.4343 30.1851,64.8193 C30.1851,64.2033 30.2811,63.6403 30.4751,63.1273 C30.6681,62.6153 30.9401,62.1743 31.2921,61.8063 C31.6441,61.4383 32.0631,61.1523 32.5541,60.9483 C33.0431,60.7453 33.5841,60.6443 34.1771,60.6443 C34.7581,60.6443 35.2701,60.7373 35.7151,60.9233 C36.1581,61.1083 36.5501,61.3613 36.8881,61.6813 L36.5231,62.1873 C36.5011,62.2253 36.4711,62.2563 36.4351,62.2813 C36.3981,62.3073 36.3511,62.3183 36.2901,62.3183 C36.2221,62.3183 36.1381,62.2813 36.0391,62.2073 C35.9401,62.1343 35.8111,62.0503 35.6511,61.9603 C35.4921,61.8683 35.2931,61.7863 35.0541,61.7123 C34.8151,61.6383 34.5201,61.6023 34.1711,61.6023 C33.7501,61.6023 33.3641,61.6743 33.0161,61.8203 C32.6661,61.9663 32.3641,62.1783 32.1121,62.4553 C31.8591,62.7323 31.6631,63.0703 31.5221,63.4693 C31.3821,63.8673 31.3111,64.3173 31.3111,64.8193 C31.3111,65.3273 31.3851,65.7813 31.5311,66.1813 C31.6781,66.5793 31.8771,66.9163 32.1291,67.1903 C32.3821,67.4663 32.6801,67.6763 33.0231,67.8193 C33.3671,67.9653 33.7381,68.0373 34.1371,68.0373 C34.3801,68.0373 34.5991,68.0233 34.7951,67.9933 C34.9901,67.9663 35.1711,67.9213 35.3361,67.8603 C35.5011,67.8003 35.6551,67.7233 35.7971,67.6303 C35.9391,67.5363 36.0811,67.4253 36.2221,67.2963 C36.2861,67.2393 36.3511,67.2113 36.4151,67.2113" id="Fill-18" fill="currentColor" />
<path d="M44.921,64.8197 C44.921,64.3177 44.853,63.8677 44.716,63.4687 C44.579,63.0707 44.386,62.7337 44.135,62.4587 C43.885,62.1827 43.581,61.9707 43.224,61.8237 C42.866,61.6747 42.469,61.6017 42.027,61.6017 C41.591,61.6017 41.194,61.6747 40.837,61.8237 C40.481,61.9707 40.176,62.1827 39.923,62.4587 C39.671,62.7337 39.476,63.0707 39.339,63.4687 C39.203,63.8677 39.135,64.3177 39.135,64.8197 C39.135,65.3207 39.203,65.7697 39.339,66.1667 C39.476,66.5627 39.671,66.8987 39.923,67.1737 C40.176,67.4497 40.481,67.6607 40.837,67.8057 C41.194,67.9527 41.591,68.0257 42.027,68.0257 C42.469,68.0257 42.866,67.9527 43.224,67.8057 C43.581,67.6607 43.885,67.4497 44.135,67.1737 C44.386,66.8987 44.579,66.5627 44.716,66.1667 C44.853,65.7697 44.921,65.3207 44.921,64.8197 M46.055,64.8197 C46.055,65.4297 45.958,65.9917 45.764,66.5027 C45.57,67.0117 45.297,67.4527 44.943,67.8197 C44.591,68.1887 44.167,68.4747 43.671,68.6777 C43.176,68.8817 42.628,68.9827 42.027,68.9827 C41.428,68.9827 40.881,68.8817 40.387,68.6777 C39.894,68.4747 39.471,68.1887 39.117,67.8197 C38.765,67.4527 38.491,67.0117 38.297,66.5027 C38.104,65.9917 38.007,65.4297 38.007,64.8197 C38.007,64.2077 38.104,63.6467 38.297,63.1347 C38.491,62.6257 38.765,62.1847 39.117,61.8147 C39.471,61.4447 39.894,61.1567 40.387,60.9517 C40.881,60.7467 41.428,60.6437 42.027,60.6437 C42.628,60.6437 43.176,60.7467 43.671,60.9517 C44.167,61.1567 44.591,61.4447 44.943,61.8147 C45.297,62.1847 45.57,62.6257 45.764,63.1347 C45.958,63.6467 46.055,64.2077 46.055,64.8197" id="Fill-20" fill="currentColor" />
<g id="Group-24" transform="translate(48.000000, 59.988500)">
<mask id="mask-8" fill="currentColor">
<use xlink:href="#path-7" />
</mask>
<g id="Clip-23" />
<path d="M4.1934,6.2462 C4.2344,6.3472 4.2764,6.4462 4.3184,6.5452 C4.3564,6.4422 4.3964,6.3412 4.4374,6.2402 C4.4794,6.1402 4.5254,6.0432 4.5744,5.9532 L7.3424,0.9342 C7.3884,0.8482 7.4384,0.7942 7.4934,0.7752 C7.5484,0.7562 7.6254,0.7472 7.7244,0.7472 L8.5394,0.7472 L8.5394,8.9082 L7.5704,8.9082 L7.5704,2.9112 C7.5704,2.8312 7.5724,2.7462 7.5764,2.6542 C7.5804,2.5632 7.5864,2.4712 7.5934,2.3752 L4.7974,7.4782 C4.7104,7.6492 4.5764,7.7352 4.3984,7.7352 L4.2384,7.7352 C4.0594,7.7352 3.9264,7.6492 3.8394,7.4782 L0.9804,2.3582 C0.9924,2.4582 1.0004,2.5542 1.0064,2.6482 C1.0114,2.7442 1.0144,2.8312 1.0144,2.9112 L1.0144,8.9082 L0.0474,8.9082 L0.0474,0.7472 L0.8604,0.7472 C0.9604,0.7472 1.0354,0.7562 1.0894,0.7752 C1.1414,0.7942 1.1934,0.8482 1.2424,0.9342 L4.0624,5.9582 C4.1074,6.0492 4.1514,6.1442 4.1934,6.2462" id="Fill-22" fill="currentColor" mask="url(#mask-8)" />
</g>
</g>
</svg>
</Button.Icon>
<Button.Label
class="text-ellipsis whitespace-nowrap"
isLoading={isGHLoading.value}>
Link your GOG.com account
</Button.Label>
<div class="w-[2%]" />
</Button.Root>
{flow == "join" ? (
<p class='max-w-[400px] pt-4 text-center font-normal text-sm dark:text-primary-50/70 text-primary-950/70'>
By creating an account, you agree to our <br />
<Link
href="/terms"
class={buttonVariants.link()}
>Terms of Service
</Link> and <Link
href="/privacy"
class={buttonVariants.link()}
>Privacy Policy
</Link>
</p>
) : (
<div class="h-14" />
)}
</div>
</div>
</>
)
})

View File

@@ -1,8 +1,10 @@
import { Avatar } from "@nestri/ui";
// import { } from "@qwik-ui/headless";
import { component$ } from "@builder.io/qwik";
import { HomeNavBar, Modal, SimpleFooter } from "@nestri/ui";
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 = [
{
@@ -52,60 +54,96 @@ const games = [
]
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">
<HomeNavBar />
<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">
<button class="border-gray-400/70 dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none group transition-all duration-200 border-[2px] h-14 rounded-xl px-4 gap-2 flex items-center justify-between overflow-hidden bg-white dark:bg-black hover:bg-gray-300/70 dark:hover:bg-gray-700/70 disabled:opacity-50">
<div class="py-2 w-2/3 flex flex-col">
<p class="text-text-100 shrink truncate w-full flex">DESKTOP-EUO8VSF</p>
</div>
<div
style={{
"--cutout-avatar-percentage-visible": 0.2,
"--head-margin-percentage": 0.1,
"--size": "3rem"
}}
class="relative h-full flex w-1/3 justify-end">
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/1085660/9f2d65473912e04aea5b63378def39dc71be2485.ico" class="h-12 shadow-lg shadow-gray-900 ring-gray-400/70 ring-1 bg-black w-12 translate-y-4 rotate-[14deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
</div>
</button>
<button class="border-gray-400/70 dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] group transition-all duration-200 border-[2px] h-14 rounded-xl px-4 gap-2 flex items-center justify-between overflow-hidden bg-white dark:bg-black hover:bg-gray-300/70 dark:hover:bg-gray-700/70 outline-none disabled:opacity-50">
<div class="py-2 w-2/3 flex flex-col">
<p class="text-text-100 shrink truncate w-full flex">DESKTOP-TYUO8VSF</p>
</div>
<div
style={{
"--cutout-avatar-percentage-visible": 0.2,
"--head-margin-percentage": 0.1,
"--size": "3rem"
}}
class="relative h-full flex w-1/3 justify-end">
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/578080/f962202b06de547cf47c156bdd7aaa5bf7f2cdbb.ico" class=" h-12 bg-black ring-gray-400/70 ring-1 shadow-lg shadow-gray-900 w-12 translate-y-4 mr-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))] rotate-12 rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/1086940/ea19a7ce2af83c0240e775d79d3b690751a062c1.ico" class="h-12 bg-black ring-gray-400/70 ring-1 shadow-lg shadow-gray-900 w-12 translate-y-4 rotate-[14deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
</div>
</button>
<button class="border-gray-400/70 dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] group transition-all duration-200 border-[2px] h-14 rounded-xl pl-4 gap-2 flex items-center justify-between overflow-hidden bg-white dark:bg-black hover:bg-gray-300/70 dark:hover:bg-gray-700/70 outline-none disabled:opacity-50">
<div class="py-2 w-2/3 flex flex-col">
<p class="text-text-100 shrink truncate w-full flex">DESKTOP-aEFO8VSF</p>
</div>
<div
style={{
"--cutout-avatar-percentage-visible": 0.2,
"--head-margin-percentage": 0.1,
"--size": "3rem"
}}
class="relative h-full flex w-1/3 justify-end">
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/2767030/bd22e45404f4ed4f3c549b575e23ce76fe03fb07.ico" class=" h-12 bg-black ring-gray-400/70 ring-1 shadow-lg shadow-gray-900 w-12 mr-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))] translate-y-4 rotate-[10deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/578080/f962202b06de547cf47c156bdd7aaa5bf7f2cdbb.ico" class=" h-12 bg-black ring-gray-400/70 ring-1 shadow-lg shadow-gray-900 w-12 mr-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))] translate-y-4 rotate-[12deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/1623730/22a20bdaa6d782f60caa45eb7b02fc2411dcd988.ico" class=" h-12 bg-black ring-gray-400/70 ring-1 shadow-lg shadow-gray-900 w-12 translate-y-4 rotate-[14deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
</div>
</button>
<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">
<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
@@ -137,7 +175,7 @@ export default component$(() => {
</Modal.Panel>
</Modal.Root>
</div>
</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 ">
@@ -176,15 +214,15 @@ export default component$(() => {
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) * Math.floor(100 * Math.random())).toString()} />
<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 * Math.floor(100 * Math.random())).toString()} />
<Avatar name={((key + 1) * random).toString()} />
</div>
</div>
<p class="font-normal text-gray-600 dark:text-gray-400 text-sm w-full truncate">{`${Math.floor(Math.random() * 100)} people are currently playing this game`}</p>
<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>
@@ -210,7 +248,7 @@ export default component$(() => {
</div>
</div>
</div>
<div class="p-4 pt-0 gap-6 flex flex-col text-white" >
<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>
@@ -287,13 +325,13 @@ export default component$(() => {
<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-300/70
<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-[#fffd]
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%)]" >
<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)`
@@ -329,7 +367,7 @@ export default component$(() => {
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_#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" >
<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>
@@ -341,8 +379,112 @@ export default component$(() => {
))}
</ul>
</div>
</section>
</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 >
)
})

View File

@@ -0,0 +1,17 @@
import type Nestri from "@nestri/sdk"
import { component$, Slot } from "@builder.io/qwik";
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
if (!currentProfile) {
throw redirect(308, `${url.origin}`)
}
}
export default component$(() => {
return (
<Slot />
)
})

View File

@@ -1,26 +1,14 @@
/* eslint-disable qwik/no-react-props */
import { Title, Text} from "@nestri/ui/react";
import { buttonVariants, cn } from "@nestri/ui/design";
import { component$ } from "@builder.io/qwik";
import { Link } from "@builder.io/qwik-city";
import { Title, Text } from "@nestri/ui/react";
import { component$ } from "@builder.io/qwik";
import { buttonVariants } from "@nestri/ui/design";
export default component$(() => {
return (
<div class="w-screen relative" >
{/**Gradient to hide the ending of the checkered bg at the bottom*/}
{/* <div class="absolute inset-0 dark:[background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.950,0.7)_102%)] [background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.50,0.7)_102%)] h-screen w-screen overflow-hidden max-w-[100vw] top-0 left-0 right-0 select-none" /> */}
<nav class="w-full h-[70px] lg:flex hidden sticky top-0 z-50 py-4 justify-center items-center" >
<div class="w-full left-1/2 relative -translate-x-[40%]">
<Link href="/" class={cn(buttonVariants.outlined({ intent: "neutral", size: "md" }), "w-max")}>
<svg xmlns="http://www.w3.org/2000/svg" class="size-[20px] -rotate-90" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path stroke-linejoin="round" d="m17 9.5l-5-5l-5 5" /><path d="M12 4.5v10c0 1.667-1 5-5 5" opacity=".5" /></g></svg>
{/* <svg xmlns="http://www.w3.org/2000/svg" class="size-[20px]" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10" opacity=".5" /><path stroke-linecap="round" stroke-linejoin="round" d="m15.5 9l-3 3l3 3m-4-6l-3 3l3 3" /></g></svg> */}
Go Back
</Link>
</div>
</nav>
<section class="px-4 relative lg:-top-[70px]" >
<section class="px-4 relative" >
<div class="mx-auto select-text max-w-xl py-8 [&_h1]:text-3xl flex relative gap-4 w-full flex-col" >
<Title className="py-4 text-4xl" >
Nestri's Privacy Policy
@@ -177,6 +165,7 @@ export default component$(() => {
<Text align="center" className="pt-3">
💖 Thank you for trusting Nestri with your data and gaming experience.💖
<br />
<br />
We are committed to safeguarding your personal information and ensuring your privacy. </Text>
</div>
</section>

View File

@@ -1,24 +1,13 @@
/* eslint-disable qwik/no-react-props */
import { Title, Text } from "@nestri/ui/react";
import { buttonVariants, cn } from "@nestri/ui/design";
import { component$ } from "@builder.io/qwik";
import { Link } from "@builder.io/qwik-city";
import { Title, Text } from "@nestri/ui/react";
import { component$ } from "@builder.io/qwik";
import { buttonVariants } from "@nestri/ui/design";
export default component$(() => {
return (
<div class="w-screen relative" >
{/**Gradient to hide the ending of the checkered bg at the bottom*/}
{/* <div class="absolute inset-0 dark:[background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.950,0.7)_102%)] [background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.50,0.7)_102%)] h-screen w-screen overflow-hidden max-w-[100vw] top-0 left-0 right-0 select-none" /> */}
<nav class="w-full h-[70px] lg:flex hidden sticky top-0 z-50 py-4 justify-center items-center" >
<div class="w-full left-1/2 relative -translate-x-[40%]">
<Link href="/" class={cn(buttonVariants.outlined({ intent: "neutral", size: "md" }), "w-max")}>
<svg xmlns="http://www.w3.org/2000/svg" class="size-[20px] -rotate-90" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path stroke-linejoin="round" d="m17 9.5l-5-5l-5 5" /><path d="M12 4.5v10c0 1.667-1 5-5 5" opacity=".5" /></g></svg>
{/* <svg xmlns="http://www.w3.org/2000/svg" class="size-[20px]" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10" opacity=".5" /><path stroke-linecap="round" stroke-linejoin="round" d="m15.5 9l-3 3l3 3m-4-6l-3 3l3 3" /></g></svg> */}
Go Back
</Link>
</div>
</nav>
<section class="px-4 relative lg:-top-[70px]" >
<section class="px-4 relative" >
<div class="mx-auto select-text max-w-xl overflow-x-hidden py-8 [&_h1]:text-3xl flex relative gap-4 w-full flex-col" >
<Title className="py-4 text-4xl" >
Nestri's Terms of Service
@@ -30,7 +19,7 @@ export default component$(() => {
</Text>
<Text>
Welcome to Nestri and thank you for using our services. We are an innovative cloud gaming platform that offers both self-hosted and hosted versions for gamers without GPUs.
Welcome to Nestri and thank you for using our services. We are an innovative cloud gaming platform that offers both self-hosted and hosted versions for gamers to play online with friends and family.
<br />
<br />
By using Nestri, you agree to these Terms of Service ("Terms"). If you have any questions, feel free to contact us.
@@ -38,11 +27,11 @@ export default component$(() => {
<Title>Who We Are</Title>
<Text>Nestri is an open-source cloud gaming platform that lets you play games on your own terms invite friends to join your gaming sessions, share your game library, and take even more control by hosting your own gaming server. Our hosted version is perfect for those who need GPU support, providing seamless gaming experiences for everyone.</Text>
<Text>Nestri is an open-source cloud gaming platform that lets you play games on your own terms invite friends to join your gaming sessions, share your game library, and take even more control by hosting your own gaming server. Our hosted version is perfect for those who need GPU support.</Text>
<Title>Privacy and Security</Title>
<Text>We take your privacy and security very seriously. We adhere to stringent data protection laws and ensure that all data, including games downloaded from Steam on behalf of the user, is encrypted. We also collect and log IP addresses to avoid abuse and ensure the security of our services. For more details, please review our
<Text>We take your privacy and security very seriously. We adhere to stringent data protection laws and ensure that all data, is secured. We also collect and log IP addresses to avoid abuse and ensure the security of our services. For more details, please review our
<Link
href="/privacy"
class={buttonVariants.link()}>
@@ -59,7 +48,7 @@ export default component$(() => {
<br />
<br />
<Link
href="/"
href="https://www.protondb.com/"
class={buttonVariants.link()}>
Check the list here</Link>.
</Text>
@@ -176,7 +165,7 @@ export default component$(() => {
.
</Text>
<Text className="pt-3">💖 Thank you for choosing Nestri for your cloud gaming needs! 💖</Text>
<Text align="center" className="pt-3">💖 Thank you for choosing Nestri for your cloud gaming needs! 💖</Text>
</div>
</section>
</div>

View File

@@ -1,6 +1,5 @@
import { component$ } from "@builder.io/qwik"
import { Link } from "@builder.io/qwik-city"
import { NavBar } from "@nestri/ui"
import { component$ } from "@builder.io/qwik"
import { MotionComponent, TitleSection, transition } from "@nestri/ui/react"
const blogs = [
@@ -15,7 +14,6 @@ const blogs = [
export default component$(() => {
return (
<div>
<NavBar />
<TitleSection client:load title="Blog" description="All the latest news from Nestri and the community." />
<MotionComponent
initial={{ opacity: 0, y: 100 }}

View File

@@ -1,13 +1,12 @@
/* eslint-disable qwik/jsx-img */
import { component$ } from "@builder.io/qwik";
import { Footer } from "@nestri/ui";
import { Link } from "@builder.io/qwik-city";
import { NavBar, Footer } from "@nestri/ui";
import { component$ } from "@builder.io/qwik";
import { MotionComponent, transition, TitleSection } from "@nestri/ui/react";
export default component$(() => {
return (
<>
<NavBar />
<TitleSection client:load title="Changelog" description="All the latest updates, improvements, and fixes to Nestri." />
<MotionComponent
initial={{ opacity: 0, y: 100 }}

View File

@@ -1,5 +1,5 @@
import { Footer } from "@nestri/ui";
import { component$ } from "@builder.io/qwik"
import { NavBar, Footer } from "@nestri/ui";
import { buttonVariants, cn } from "@nestri/ui/design";
import { MotionComponent, transition, TitleSection } from "@nestri/ui/react";
@@ -85,7 +85,6 @@ export default component$(() => {
return (
<>
<NavBar />
<TitleSection client:load title="Contact" description="Need help? Found a bug? Have a suggestion? Let us know!" />
<MotionComponent

View File

@@ -1,5 +1,5 @@
import { component$ } from "@builder.io/qwik"
import { NavBar, Footer } from "@nestri/ui"
import { Footer } from "@nestri/ui"
import { TitleSection, MotionComponent, transition } from "@nestri/ui/react"
//FIXME: Create and pin the Github Issue
@@ -8,7 +8,6 @@ export default component$(() => {
return (
<>
<NavBar />
<TitleSection client:load title="Fundraiser" description="Support Nestri — Make cloud gaming accessible to everyone" />
<MotionComponent
initial={{ opacity: 0, y: 100 }}
@@ -65,7 +64,7 @@ export default component$(() => {
</div>
</div>
</MotionComponent>
<Footer showGH={false} />
<Footer showBanner={false} />
</>
)
})

View File

@@ -1,8 +1,9 @@
import { component$ } from "@builder.io/qwik";
import { type DocumentHead } from "@builder.io/qwik-city";
import { HeroSection, MotionComponent, transition } from "@nestri/ui/react"
import { NavBar, Footer } from "@nestri/ui"
import { Footer } from "@nestri/ui"
import { cn } from "@nestri/ui/design";
import { component$ } from "@builder.io/qwik";
import { HeroSection, MotionComponent, transition } from "@nestri/ui/react"
import { type RequestHandler, type DocumentHead } from "@builder.io/qwik-city";
const tags = [
{
@@ -40,25 +41,49 @@ const games = [
"https://assets-prd.ignimgs.com/2023/03/22/keyart-wide-1-1679503853654-1679505306655.jpeg",
"https://assets-prd.ignimgs.com/2022/11/09/coffee-talk-episode-1-button-fin-1668033710468.jpg",
"https://assets-prd.ignimgs.com/2022/06/15/stalker2chornobyl-1655253282275.jpg",
"https://assets-prd.ignimgs.com/2022/05/24/call-of-duty-modern-warfare-2-button-02-1653417394041.jpg",
"https://assets-prd.ignimgs.com/2023/05/27/alanwake2-1685200534966.jpg",
"https://assets-prd.ignimgs.com/2023/02/16/apexrevelry-1676588335122.jpg"
]
export const onGet: RequestHandler = async ({ request, send }) => {
const userAgent = request.headers.get('user-agent') || ''
const isCurl = userAgent.toLowerCase().includes('curl');
//TODO:
if (isCurl) {
const response = new Response(
`#!/bin/bash
echo "Not yet ready 😅\n"
echo "Consider joining our Discord channel (https://discord.com/invite/Y6etn3qKZ3) for the latest updates \n"
`, {
status: 200,
headers: {
'Content-Type': 'text/plain',
'Content-Disposition': 'attachment; filename="nestri.sh"'
}
})
send(response)
}
};
// FIXME: Change up the copy
//TODO: Use a db to query all this
//TODO: Add the search modal
// TODO: Add the game modal
//TODO: Add the demo video/gif
export default component$(() => {
return (
<div class="w-screen relative">
<NavBar />
<HeroSection client:load>
<div class="w-full flex flex-col">
<button onClick$={() => null} class="group w-full max-w-xl focus:ring-primary-500 duration-200 outline-none rounded-xl flex items-center justify-start hover:bg-gray-200 focus:bg-gray-200 dark:hover:bg-gray-800 dark:focus:bg-gray-800 transition-all gap-2 px-4 py-3 h-[45px] ring-2 ring-gray-300 dark:ring-gray-700 mx-auto text-gray-900/70 dark:text-gray-100/70 bg-white dark:bg-black">
<button onClick$={() => {
//@ts-ignore
navigator.clipboard.writeText("curl -fsSL https://nestri.io | sh")
}} class="group w-full max-w-xl focus:ring-primary-500 duration-200 outline-none rounded-xl flex items-center justify-start hover:bg-gray-200 focus:bg-gray-200 dark:hover:bg-gray-800 dark:focus:bg-gray-800 transition-all gap-2 px-4 py-3 h-[45px] ring-2 ring-gray-300 dark:ring-gray-700 mx-auto text-gray-900/70 dark:text-gray-100/70 bg-white dark:bg-black">
<svg xmlns="http://www.w3.org/2000/svg" class="size-[20px] flex-shrink-0" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M3.464 3.464C2 4.93 2 7.286 2 12s0 7.071 1.464 8.535C4.93 22 7.286 22 12 22s7.071 0 8.535-1.465C22 19.072 22 16.714 22 12s0-7.071-1.465-8.536C19.072 2 16.714 2 12 2S4.929 2 3.464 3.464m2.96 6.056a.75.75 0 0 1 1.056-.096l.277.23c.605.504 1.12.933 1.476 1.328c.379.42.674.901.674 1.518s-.295 1.099-.674 1.518c-.356.395-.871.824-1.476 1.328l-.277.23a.75.75 0 1 1-.96-1.152l.234-.195c.659-.55 1.09-.91 1.366-1.216c.262-.29.287-.427.287-.513s-.025-.222-.287-.513c-.277-.306-.707-.667-1.366-1.216l-.234-.195a.75.75 0 0 1-.096-1.056M17.75 15a.75.75 0 0 1-.75.75h-5a.75.75 0 0 1 0-1.5h5a.75.75 0 0 1 .75.75" clip-rule="evenodd" /></svg>
<p class="font-bold tracking-tighter h-max overflow-hidden overflow-ellipsis whitespace-nowrap font-mono">
curl -fsSL https://nestri.io/install | bash
curl -fsSL https://nestri.io | sh
</p>
<div class="ml-auto flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="group-focus:hidden size-6 flex-shrink-0" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M15.24 2h-3.894c-1.764 0-3.162 0-4.255.148c-1.126.152-2.037.472-2.755 1.193c-.719.721-1.038 1.636-1.189 2.766C3 7.205 3 8.608 3 10.379v5.838c0 1.508.92 2.8 2.227 3.342c-.067-.91-.067-2.185-.067-3.247v-5.01c0-1.281 0-2.386.118-3.27c.127-.948.413-1.856 1.147-2.593s1.639-1.024 2.583-1.152c.88-.118 1.98-.118 3.257-.118h3.07c1.276 0 2.374 0 3.255.118A3.6 3.6 0 0 0 15.24 2" /><path fill="currentColor" d="M6.6 11.397c0-2.726 0-4.089.844-4.936c.843-.847 2.2-.847 4.916-.847h2.88c2.715 0 4.073 0 4.917.847S21 8.671 21 11.397v4.82c0 2.726 0 4.089-.843 4.936c-.844.847-2.202.847-4.917.847h-2.88c-2.715 0-4.073 0-4.916-.847c-.844-.847-.844-2.21-.844-4.936z" /></svg>

View File

@@ -0,0 +1,51 @@
// 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} />
<Slot />
</>
)
})

View File

@@ -1,8 +1,10 @@
import { $, component$, noSerialize, type NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import { TitleSection, MotionComponent, transition } from "@nestri/ui/react";
import { NavBar, Footer, Book } from "@nestri/ui"
import { cn } from "@nestri/ui/design";
import { Howl } from 'howler';
import { cn } from "@nestri/ui/design";
import { Link, routeLoader$ } from "@builder.io/qwik-city";
import { Footer, Book, CONSTANTS } from "@nestri/ui"
import { TitleSection, MotionComponent, transition } from "@nestri/ui/react";
import { $, component$, noSerialize, type NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
// import { createClient } from '@openauthjs/openauth/client';
//FIXME: Add a FAQ section
// FIXME: Takes too long for the price input radio input to become responsive
@@ -62,27 +64,46 @@ const convertToTitle = (value: any) => {
}
}
// export const useLink = routeLoader$(async (ev) => {
// const client = createClient({
// clientID: "www",
// issuer: "https://auth.nestri.io"
// })
// const { url } = await client.authorize(ev.url.origin + "/callback", "code")
// return url
// })
export const useLink = routeLoader$(async ({ sharedMap }) => {
const url = sharedMap.get("auth_url") as string
return url
})
export default component$(() => {
const loginUrl = useLink() // { value: "/" }//
const priceValue = useSignal(3)
const buttonRef = useSignal<HTMLButtonElement | undefined>()
const bookRef = useSignal<HTMLButtonElement | undefined>()
const docsLinkRef = useSignal<HTMLElement | undefined>()
const bookRef = useSignal<HTMLElement | undefined>()
const audio = useSignal<NoSerialize<Howl> | undefined>()
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(() => {
audio.value = noSerialize(new Howl({ src: ["/audio/click.wav"], volume: 0.5 }))
audio.value = noSerialize(new Howl({ src: ["/audio/click.wav"] }))
buttonRef.value?.addEventListener("mouseenter", () => {
docsLinkRef.value?.addEventListener("mouseenter", () => {
bookRef.value?.classList.add('flip')
})
buttonRef.value?.addEventListener("mouseleave", () => {
docsLinkRef.value?.addEventListener("mouseleave", () => {
bookRef.value?.classList.remove('flip')
})
return () => {
buttonRef.value?.removeEventListener("mouseenter", () => {
docsLinkRef.value?.removeEventListener("mouseenter", () => {
bookRef.value?.classList.add('flip')
})
buttonRef.value?.removeEventListener("mouseleave", () => {
docsLinkRef.value?.removeEventListener("mouseleave", () => {
bookRef.value?.classList.remove('flip')
})
}
@@ -95,7 +116,6 @@ export default component$(() => {
return (
<>
<NavBar />
<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 }}
@@ -125,11 +145,11 @@ export default component$(() => {
<div class="flex flex-col w-full">
<p class="text-[4rem] leading-[1] font-medium font-title"> Free </p>
{/**FIXME: Add the link to the docs here */}
<button ref={v => bookRef.value = v} class="h-[154px] w-full flex items-start pt-4 justify-center overflow-hidden">
<a href={CONSTANTS.githubLink} ref={v => bookRef.value = v} class="h-[154px] w-full flex items-start pt-4 justify-center overflow-hidden">
<Book textColor="#FFF"
bgColor="#FF4F01"
title="Getting started with Nestri" class="shadow-lg shadow-gray-900 dark:shadow-gray-300" />
</button>
</a>
<hr class="h-[2px] bg-gray-400 text-gray-300 dark:bg-gray-600 " />
</div>
<div class="w-full relative sm:text-sm text-base gap-3 flex flex-col">
@@ -254,20 +274,20 @@ export default component$(() => {
</div>
</div>
</div>
<button ref={v => buttonRef.value = v} class="my-4 bg-white dark:bg-black focus:ring-primary-500 hover:ring-primary-500 ring-gray-500 rounded-lg outline-none dark:text-gray-100/70 ring-2 text-sm h-max py-2 px-4 flex items-center transition-all duration-200 focus:bg-primary-100 focus:dark:bg-primary-900 focus:text-primary-500 text-gray-500 font-title font-bold justify-between">
<a href={CONSTANTS.githubLink} ref={v => docsLinkRef.value = v} class="my-4 bg-white dark:bg-black focus:ring-primary-500 hover:ring-primary-500 ring-gray-500 rounded-lg outline-none dark:text-gray-100/70 ring-2 text-sm h-max py-2 px-4 flex items-center transition-all duration-200 focus:bg-primary-100 focus:dark:bg-primary-900 focus:text-primary-500 text-gray-500 font-title font-bold justify-between">
Read the Docs
<div class="size-5 relative">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
</svg>
</div>
</button>
</a>
</div>
<div class="gap-3 w-full p-6 flex flex-col rounded-lg bg-white dark:bg-black">
<div class="flex items-center font-title h-min w-full justify-between">
<div class="flex items-center justify-center gap-2 ">
<div class="bg-gradient-to-t from-[#685fea] to-[rgb(153,148,224)] rounded-full h-4 w-4" />
<h1 class="text-base font-semibold">Family</h1>
<h1 class="text-base font-semibold">Pro</h1>
</div>
</div>
<div class="break-words [word-break:break-word] [text-wrap:balance] [word-wrap:break-word] w-full relative whitespace-pre-wrap">
@@ -329,7 +349,7 @@ export default component$(() => {
class="overflow-hidden absolute cursor-pointer z-30 top-0 left-0 opacity-0 h-full w-full"
/>
</div>
<div class="flex justify-center items-center w-full h-[72px] mt-2.5">
<div class="flex justify-center items-center w-full h-[72px] mt-2.5 text-primary-500">
<p class="font-title text-lg font-bold text-center h-max break-words whitespace-pre-line">{convertToTitle(priceValue.value)}</p>
</div>
</div>
@@ -455,14 +475,14 @@ export default component$(() => {
</div>
</div>
</div>
<button class="my-4 focus:ring-primary-500 hover:ring-primary-500 ring-gray-500 rounded-lg outline-none dark:text-gray-100/70 ring-2 text-sm h-max py-2 px-4 flex items-center transition-all duration-200 focus:bg-primary-100 focus:dark:bg-primary-900 bg-gray-300/70 dark:bg-gray-700/30 focus:text-primary-500 text-gray-500 font-title font-bold justify-between">
Start Playing with Family Now
<a href={loginUrl.value} class="my-4 focus:ring-primary-500 hover:ring-primary-500 ring-gray-500 rounded-lg outline-none dark:text-gray-100/70 ring-2 text-sm h-max py-2 px-4 flex items-center transition-all duration-200 focus:bg-primary-100 focus:dark:bg-primary-900 bg-gray-300/70 dark:bg-gray-700/30 focus:text-primary-500 text-gray-500 font-title font-bold justify-between">
Get Nestri Pro
<div class="size-5 relative">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
</svg>
</div>
</button>
</a>
<div class="flex flex-col gap-0.5">
<div class="text-neutral-900/70 dark:text-neutral-100/70 text-xs">
<sup>1</sup> Feature is in development
@@ -483,15 +503,15 @@ export default component$(() => {
<p class="text-neutral-900/70 dark:text-neutral-100/70 text-base" >
Looking for something else? Use Nestri as your own on our servers or yours. Flexible licensing and white-glove onboarding included.
</p>
<button class="underline underline-offset-2 font-medium font-title hover:opacity-70 w-max">
<Link href={CONSTANTS.enterpriseContact} class="underline underline-offset-2 font-medium font-title hover:opacity-70 w-max">
Contact Sales
</button>
</Link>
</div>
<div class="w-full text-gray-900/70 bg-gray-400/30 dark:bg-gray-600/30 dark:text-gray-100/30 whitespace-nowrap font-mono text-sm mt-6 py-3">
<div class="flex relative">
<span class="whitespace-pre marquee-animation">
Organization Account · Security Restrictions · Custom Events · Single Sign On · Advanced Integrations · Additional APIs · Custom-Built Features ·
Organization Account · Security Restrictions · Custom Events · Single Sign On · Advanced Integrations · Additional APIs · Custom-Built Features ·
Organization Account · Security Restrictions · Custom Parties · Single Sign On · Advanced Integrations · Additional APIs · Custom-Built Features ·
Organization Account · Security Restrictions · Custom Parties · Single Sign On · Advanced Integrations · Additional APIs · Custom-Built Features ·
</span>
</div>
</div>

View File

@@ -0,0 +1,85 @@
import Nestri from '@nestri/sdk';
import { routeLoader$ } from '@builder.io/qwik-city';
import { component$, useVisibleTask$ } from '@builder.io/qwik';
// function getCookie(cname: string) {
// const name = cname + "=";
// const ca = document.cookie.split(';');
// for (let i = 0; i < ca.length; i++) {
// let c = ca[i];
// while (c.charAt(0) == ' ') {
// c = c.substring(1);
// }
// if (c.indexOf(name) == 0) {
// return c.substring(name.length, c.length);
// }
// }
// return "";
// }
// function getParams(url: URL) {
// const urlString = url.toString()
// const hash = urlString.substring(urlString.indexOf('?') + 1); // Extract the part after the #
// const params = new URLSearchParams(hash);
// const paramsObj = {} as any;
// for (const [key, value] of params.entries()) {
// paramsObj[key] = decodeURIComponent(value);
// }
// console.log(paramsObj)
// return paramsObj;
// }
//FIXME: There is an issue where the cookie cannot be found, tbh, i dunno what drugs Qwik is on
export const useSubscribe = routeLoader$(async ({ url, cookie }) => {
const access = cookie.get("access_token")
if (access) {
const bearerToken = access.value
console.log("bearerToken", bearerToken)
const nestriClient = new Nestri({
bearerToken,
baseURL: "https://api.nestri.io"
})
const checkout_id = url.searchParams.get("checkout")
if (checkout_id) {
console.log("checkout", checkout_id)
await nestriClient.subscriptions.create({
checkoutID: checkout_id
})
return "okey"
}
}
})
export default component$(() => {
const subscribe = useSubscribe()
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(async () => {
console.log("subscribe", subscribe)
})
// const bearerToken = getCookie("access_token")
// // console.log("bearerToken", bearerToken)
// const nestriClient = new Nestri({
// bearerToken,
// baseURL: "https://api.lauryn.dev.nestri.io"
// })
// const urlObj = new URL(window.location.href)
// const checkout_id = getParams(urlObj).checkout
// if (checkout_id) {
// await nestriClient.subscriptions.create({
// checkoutID: checkout_id
// })
// }
// })
return (
<div class="w-screen h-full pt-20 flex justify-center items-center" >
<h1 class="text-3xl">Thank you, now check your email for more details</h1>
</div>
)
})

View File

@@ -0,0 +1,73 @@
import Nestri from "@nestri/sdk";
import { component$, useVisibleTask$ } from "@builder.io/qwik";
import { createClient } from "@openauthjs/openauth/client";
import { routeLoader$, useNavigate, type CookieOptions } from "@builder.io/qwik-city";
export const useLoggedIn = routeLoader$(async ({ query, url, cookie }) => {
const code = query.get("code")
if (code) {
const redirect_uri = url.origin + "/callback"
const cookieOptions: CookieOptions = {
path: "/",
sameSite: "lax",
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
}
const client = createClient({
clientID: "www",
issuer: "https://auth.nestri.io"
})
const tokens = await client.exchange(code, redirect_uri)
if (!tokens.err) {
const access_token = tokens.tokens.access
const refresh_token = tokens.tokens.refresh
cookie.set("access_token", access_token, cookieOptions)
cookie.set("refresh_token", refresh_token, cookieOptions)
const bearerToken = access_token
const nestriClient = new Nestri({
bearerToken,
baseURL: "https://api.nestri.io"
})
//TODO: Use subjects instead
const currentProfile = await nestriClient.users.retrieve()
const username = currentProfile.data.username
return username
}
}
})
export default component$(() => {
const username = 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);
}
})
return (
<div class="w-screen h-screen 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>
We are confirming your identity...</span>
</div>
)
})

View File

@@ -1,6 +1,7 @@
import { component$, Slot } from "@builder.io/qwik";
import Nestri from "@nestri/sdk";
import { NavProgress } from "@nestri/ui";
import type { DocumentHead, RequestHandler } from "@builder.io/qwik-city";
import { component$, Slot } from "@builder.io/qwik";
import { type DocumentHead, type RequestHandler } from "@builder.io/qwik-city";
export const onGet: RequestHandler = async ({ cacheControl }) => {
// Control caching for this request for best performance and to reduce hosting costs:
@@ -13,6 +14,21 @@ export const onGet: RequestHandler = async ({ cacheControl }) => {
});
};
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.nestri.io"
})
const currentProfile = await nestriClient.users.retrieve()
sharedMap.set("profile", currentProfile.data)
}
}
export default component$(() => {
return (
<>

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,10 +0,0 @@
import { auth, urls } from "./api"
// export const cmd = new sst.x.DevCommand("Cmd", {
// link: [urls, auth],
// dev: {
// autostart: true,
// command: "cd packages/cmd && go run main.go"
// }
// })

View File

@@ -1,6 +1,6 @@
export const domain =
{
production: "prod.nestri.io", //temporary use until we go into the real production
production: "nestri.io",
dev: "dev.nestri.io",
}[$app.stage] || $app.stage + ".dev.nestri.io";

View File

@@ -14,11 +14,17 @@ const _schema = i.schema({
profiles: i.entity({
avatarUrl: i.string().optional(),
username: i.string().indexed(),
ownerID: i.string().unique().indexed(),
updatedAt: i.date(),
createdAt: i.date(),
discriminator: i.string().indexed()
}),
teams: i.entity({
name: i.string(),
slug: i.string().unique().indexed(),
deletedAt: i.date().optional().indexed(),
updatedAt: i.date(),
createdAt: i.date(),
}),
games: i.entity({
name: i.string(),
steamID: i.number().unique().indexed(),
@@ -29,12 +35,31 @@ const _schema = i.schema({
endedAt: i.date().optional().indexed(),
public: i.boolean().indexed(),
}),
subscriptions: i.entity({
checkoutID: i.string(),
// quantity: i.number(),
// frequency: i.string(),
canceledAt: i.date(),
// next: i.date()
})
},
links: {
UserSubscriptions: {
forward: { on: "subscriptions", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "subscriptions" }
},
UserProfiles: {
forward: { on: "profiles", has: "one", label: "owner" },
reverse: { on: "$users", has: "one", label: "profile" }
},
TeamsOwned: {
forward: { on: "teams", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "teamsOwned" },
},
TeamsJoined: {
forward: { on: "teams", has: "many", label: "members" },
reverse: { on: "$users", has: "many", label: "teamsJoined" },
},
UserMachines: {
forward: { on: "machines", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "machines" }

View File

@@ -22,4 +22,24 @@ export namespace Email {
console.log("error sending email", error)
}
}
export async function sendWelcome(
to: string,
name: string,
) {
try {
await Client().sendTransactionalEmail(
{
transactionalId: "cm61jrbbx02twlstfwfcywt5u",
email: to,
dataVariables: {
name
}
}
);
} catch (error) {
console.log("error sending email", error)
}
}
}

View File

@@ -14,6 +14,25 @@ export module Examples {
updatedAt: '2025-01-09T01:56:23.902Z'
}
export const Subscription = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
checkoutID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
// productID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
// quantity: 1,
// frequency: "monthly" as const,
// next: '2025-01-09T01:56:23.902Z',
canceledAt: '2025-02-09T01:56:23.902Z'
}
export const Team = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
owner: true,
name: "Jane Doe's Games",
slug: "jane-does-games",
createdAt: '2025-01-04T11:56:23.902Z',
updatedAt: '2025-01-09T01:56:23.902Z'
}
export const Machine = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "DESKTOP-EUO8VSF",

View File

@@ -5,6 +5,7 @@ import { Examples } from "../examples";
import databaseClient from "../database";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID } from "@instantdb/admin";
import { useCurrentUser } from "../actor";
export module Profiles {
const MAX_ATTEMPTS = 50;
@@ -124,10 +125,6 @@ export module Profiles {
export const create = fn(z.object({ username: z.string(), customDiscriminator: z.string().optional(), avatarUrl: z.string().optional(), owner: z.string() }), async (input) => {
const username = sanitizeUsername(input.username);
// if (!username || username.length < 2 || username.length > 32) {
// // throw new Error('Invalid username length');
// }
const db = databaseClient()
const id = createID()
const now = new Date().toISOString()
@@ -150,6 +147,7 @@ export module Profiles {
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length != 0) {
@@ -176,7 +174,6 @@ export module Profiles {
avatarUrl: input.avatarUrl,
createdAt: now,
updatedAt: now,
ownerID: input.owner,
discriminator,
}).link({ owner: input.owner })
)
@@ -214,7 +211,7 @@ export module Profiles {
profiles: {
$: {
where: {
ownerID
owner: ownerID
}
},
}
@@ -227,6 +224,27 @@ export module Profiles {
return null
}
return profiles
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator
}))
)
return profile[0]
}
export const getCurrentProfile = async () => {
const user = useCurrentUser()
const currentProfile = await getProfile(user.id);
return currentProfile
}
};

View File

@@ -0,0 +1,205 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Common } from "../common";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { Email } from "../email";
import { Profiles } from "../profile";
export const SubscriptionFrequency = z.enum([
"fixed",
"daily",
"weekly",
"monthly",
"yearly",
]);
export type SubscriptionFrequency = z.infer<typeof SubscriptionFrequency>;
export namespace Subscriptions {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Subscription.id,
}),
checkoutID: z.string().openapi({
description: "The polar.sh checkout id",
example: Examples.Subscription.checkoutID,
}),
// productID: z.string().openapi({
// description: "ID of the product being subscribed to.",
// example: Examples.Subscription.productID,
// }),
// quantity: z.number().int().openapi({
// description: "Quantity of the subscription.",
// example: Examples.Subscription.quantity,
// }),
// frequency: SubscriptionFrequency.openapi({
// description: "Frequency of the subscription.",
// example: Examples.Subscription.frequency,
// }),
// next: z.string().or(z.number()).openapi({
// description: "Next billing date for the subscription.",
// example: Examples.Subscription.next,
// }),
canceledAt: z.string().or(z.number()).optional().openapi({
description: "Cancelled date for the subscription.",
example: Examples.Subscription.canceledAt,
}),
})
.openapi({
ref: "Subscription",
description: "Subscription to a Nestri product.",
example: Examples.Subscription,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
owner: user.id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
// next: group[0].next,
// frequency: group[0].frequency as any,
// quantity: group[0].quantity,
// productID: group[0].productID,
checkoutID: group[0].checkoutID,
}))
)
return result
}
export const create = fn(Info.omit({ id: true, canceledAt: true }), async (input) => {
// const id = createID()
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
//Use the polar.sh ID
await db.transact(db.tx.subscriptions[id]!.update({
// next: input.next,
// frequency: input.frequency,
// quantity: input.quantity,
checkoutID: input.checkoutID,
}).link({ owner: user.id }))
const res = await db.auth.getUser({ id: user.id })
const profile = await Profiles.getProfile(user.id)
if (profile) {
await Email.sendWelcome(res.email, profile.username)
}
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
await db.transact(db.tx.subscriptions[id]!.update({
canceledAt: new Date().toString()
}))
})
export const fromID = fn(z.string(), async (id) => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
id,
//Make sure they can only get subscriptions they own
owner: user.id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
checkoutID: group[0].checkoutID,
// next: group[0].next,
// frequency: group[0].frequency as any,
// quantity: group[0].quantity,
// productID: group[0].productID,
}))
)
return result[0]
})
export const fromCheckoutID = fn(z.string(), async (id) => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
subscriptions: {
$: {
where: {
id,
//Make sure they can only get subscriptions they own
checkoutID: id,
canceledAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const response = res.subscriptions
if (!response || response.length === 0) {
return null
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
checkoutID: group[0].checkoutID,
}))
)
return result[0]
})
}

View File

@@ -0,0 +1,165 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Common } from "../common";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
export namespace Teams {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Team.id,
}),
name: z.string().openapi({
description: "Name of the team",
example: Examples.Team.name,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this team was first created",
example: Examples.Team.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this team was last edited",
example: Examples.Team.updatedAt,
}),
owner: z.boolean().openapi({
description: "Whether this team is owned by this user",
example: Examples.Team.owner,
}),
slug: z.string().openapi({
description: "This is the unique name identifier for the team",
example: Examples.Team.slug
})
})
.openapi({
ref: "Team",
description: "A group of users sharing the same machines for gaming.",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
const query = {
teams: {
$: {
where: {
members: user.id,
deletedAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
slug: group[0].slug,
//@ts-expect-error
owner: group[0].owner === user.id
}))
)
return result
}
export const fromSlug = fn(z.string(), async (slug) => {
const db = databaseClient()
const query = {
teams: {
$: {
where: {
slug,
deletedAt: { $isNull: true }
}
},
}
}
const res = await db.query(query)
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
slug: group[0].slug,
updatedAt: group[0].updatedAt,
//@ts-expect-error
owner: group[0].owner === user.id
}))
)
return result[0]
})
export const create = fn(Info.pick({ name: true, slug: true }), async (input) => {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
name: input.name,
slug: input.slug,
createdAt: now,
updatedAt: now,
}).link({ owner: user.id, members: user.id }))
return id
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
deletedAt: now
}))
return "ok"
})
export const invite = fn(z.object({email:z.string(), id: z.string()}), async (input) => {
//TODO:
// const db = databaseClient()
// const now = new Date().toISOString()
// await db.transact(db.tx.teams[id]!.update({
// deletedAt: now
// }))
return "ok"
})
}

View File

@@ -4,7 +4,7 @@ import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
export module User {
export module Users {
export const Info = z
.object({
id: z.string().openapi({

View File

@@ -1,12 +1,15 @@
import "zod-openapi/extend";
import { Resource } from "sst";
import { ZodError } from "zod";
import { UserApi } from "./user";
import { GameApi } from "./game";
import { TeamApi } from "./team";
import { logger } from "hono/logger";
import { subjects } from "../subjects";
import { SessionApi } from "./session";
import { MachineApi } from "./machine";
import { openAPISpecs } from "hono-openapi";
import { SubscriptionApi } from "./subscription";
import { VisibleError } from "@nestri/core/error";
import { ActorContext } from '@nestri/core/actor';
import { Hono, type MiddlewareHandler } from "hono";
@@ -81,9 +84,12 @@ app
.use(auth);
const routes = app
.route("/users", UserApi.route)
.route("/teams", TeamApi.route)
.route("/games", GameApi.route)
.route("/machines", MachineApi.route)
.route("/sessions", SessionApi.route)
.route("/machines", MachineApi.route)
.route("/subscriptions", SubscriptionApi.route)
.onError((error, c) => {
console.warn(error);
if (error instanceof VisibleError) {

View File

@@ -166,11 +166,11 @@ export module SessionApi {
},
)
.post(
"/:id",
"/",
describeRoute({
tags: ["Session"],
summary: "Create a new gaming session for this user",
description: "Creates a new gaming session for the currently authenticated user, enabling them to play a game",
description: "Create a new gaming session for the currently authenticated user, enabling them to play a game",
responses: {
200: {
content: {

View File

@@ -0,0 +1,132 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
import { Subscriptions } from "@nestri/core/subscription/index";
import { Email } from "@nestri/core/email/index";
export module SubscriptionApi {
export const route = new Hono()
.get(
"/",
describeRoute({
tags: ["Subscription"],
summary: "List subscriptions",
description: "List the subscriptions associated with the current user.",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Subscriptions.Info.array().openapi({
description: "List of subscriptions.",
example: [Examples.Subscription],
}),
),
},
},
description: "List of subscriptions.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No subscriptions found for this user",
},
},
}),
async (c) => {
const data = await Subscriptions.list();
if (!data) return c.json({ error: "No subscriptions found for this user" }, 404);
return c.json({ data }, 200);
},
)
.post(
"/",
describeRoute({
tags: ["Subscription"],
summary: "Subscribe",
description: "Create a subscription for the current user.",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "Subscription was created successfully.",
},
400: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Subscription already exists.",
},
},
}),
validator(
"json",
z.object({
checkoutID: Subscriptions.Info.shape.id.openapi({
description: "The checkout id information.",
example: Examples.Subscription.id,
})
}),
),
async (c) => {
const body = c.req.valid("json");
const data = await Subscriptions.fromCheckoutID(body.checkoutID)
if (data) return c.json({ error: "Subscription already exists" })
await Subscriptions.create(body);
return c.json({ data: "ok" as const }, 200);
},
)
.delete(
"/:id",
describeRoute({
tags: ["Subscription"],
summary: "Cancel",
description: "Cancel a subscription for the current user.",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "Subscription was cancelled successfully.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Subscription not found.",
},
},
}),
validator(
"param",
z.object({
id: Subscriptions.Info.shape.id.openapi({
description: "ID of the subscription to cancel.",
example: Examples.Subscription.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const subscription = await Subscriptions.fromID(param.id);
if (!subscription) return c.json({ error: "Subscription not found" }, 404);
await Subscriptions.remove(param.id);
return c.json({ data: "ok" as const }, 200);
},
);
}

View File

@@ -0,0 +1,238 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Teams } from "@nestri/core/team/index";
import { Users } from "@nestri/core/user/index";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
export module TeamApi {
export const route = new Hono()
.get(
"/",
//FIXME: Add a way to filter through query params
describeRoute({
tags: ["Team"],
summary: "Retrieve all teams",
description: "Returns a list of all teams which the authenticated user is part of",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Teams.Info.array().openapi({
description: "A list of teams associated with the user",
example: [Examples.Team],
}),
),
},
},
description: "Successfully retrieved the list teams",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No teams found for the authenticated user",
},
},
}),
async (c) => {
const teams = await Teams.list();
if (!teams) return c.json({ error: "No teams found for this user" }, 404);
return c.json({ data: teams }, 200);
},
)
.get(
"/:slug",
describeRoute({
tags: ["Team"],
summary: "Retrieve a team by slug",
description: "Fetch detailed information about a specific team using its unique slug",
responses: {
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No team found matching the provided slug",
},
200: {
content: {
"application/json": {
schema: Result(
Teams.Info.openapi({
description: "Detailed information about the requested team",
example: Examples.Team,
}),
),
},
},
description: "Successfully retrieved the team information",
},
},
}),
validator(
"param",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique slug used to identify the team",
example: Examples.Team.slug,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const team = await Teams.fromSlug(params.slug);
if (!team) return c.json({ error: "Team not found" }, 404);
return c.json({ data: team }, 200);
},
)
.post(
"/",
describeRoute({
tags: ["Team"],
summary: "Create a team",
description: "Create a new team for the currently authenticated user, enabling them to invite and play a game together with friends",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok"))
},
},
description: "Team successfully created",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A team with this slug already exists",
},
},
}),
validator(
"json",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique name to be used with this team",
example: Examples.Team.slug
}),
name: Teams.Info.shape.name.openapi({
description: "The human readable name to give this team",
example: Examples.Team.name
})
})
),
async (c) => {
const params = c.req.valid("json")
const team = await Teams.fromSlug(params.slug)
if (team) return c.json({ error: "A team with this slug already exists" }, 404);
const res = await Teams.create(params)
return c.json({ data: res }, 200);
},
)
.delete(
"/:slug",
describeRoute({
tags: ["Team"],
summary: "Delete a team",
description: "This endpoint allows a user to delete a team, by providing it's unique slug",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "The team was successfully deleted.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A team with this slug does not exist",
},
401: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Your are not authorized to delete this team",
},
}
}),
validator(
"param",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique slug of the team to be deleted. ",
example: Examples.Team.slug,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const team = await Teams.fromSlug(params.slug)
if (!team) return c.json({ error: "Team not found" }, 404);
if (!team.owner) return c.json({ error: "Your are not authorised to delete this team" }, 401)
const res = await Teams.remove(team.id);
return c.json({ data: res }, 200);
},
)
.post(
"/:slug/invite/:email",
describeRoute({
tags: ["Team"],
summary: "Invite a user to a team",
description: "Invite a user to a team owned by the current user",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "User successfully invited",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The game with the specified Steam ID was not found",
},
}
}),
validator(
"param",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique slug of the team the user wants to invite ",
example: Examples.Team.slug,
}),
email: Users.Info.shape.email.openapi({
description: "The email of the user to invite",
example: Examples.User.email
})
}),
),
async (c) => {
const params = c.req.valid("param");
const team = await Teams.fromSlug(params.slug)
if (!team) return c.json({ error: "Team not found" }, 404);
if (!team.owner) return c.json({ error: "Your are not authorized to delete this team" }, 401)
return c.json({ data: "ok" }, 200);
},
)
}

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { Profiles } from "@nestri/core/profile/index";
import { validator, resolver } from "hono-openapi/zod";
export module UserApi {
export const route = new Hono()
.get(
"/@me",
describeRoute({
tags: ["User"],
summary: "Retrieve current user profile",
description: "Returns the current authenticate user's profile",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Profiles.Info.openapi({
description: "The profile for this user",
example: Examples.Profile,
}),
),
},
},
description: "Successfully retrieved the user's profile",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No user profile found",
},
},
}), async (c) => {
const profile = await Profiles.getCurrentProfile();
if (!profile) return c.json({ error: "No profile found for this user" }, 404);
return c.json({ data: profile }, 200);
},
)
}

View File

@@ -6,7 +6,11 @@ import {
import { Select } from "./ui/select";
import { subjects } from "./subjects"
import { PasswordUI } from "./ui/password"
import { Email } from "@nestri/core/email/index"
import { Users } from "@nestri/core/user/index"
import { authorizer } from "@openauthjs/openauth"
import { Profiles } from "@nestri/core/profile/index"
import { handleDiscord, handleGithub } from "./utils";
import { type CFRequest } from "@nestri/core/types"
import { GithubAdapter } from "./ui/adapters/github";
import { DiscordAdapter } from "./ui/adapters/discord";
@@ -14,9 +18,6 @@ import { Machines } from "@nestri/core/machine/index"
import { PasswordAdapter } from "./ui/adapters/password"
import { type Adapter } from "@openauthjs/openauth/adapter/adapter"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { handleDiscord, handleGithub } from "./utils";
import { User } from "@nestri/core/user/index"
import { Profiles } from "@nestri/core/profile/index"
interface Env {
CloudflareAuthKV: KVNamespace
}
@@ -89,7 +90,7 @@ export default {
PasswordUI({
sendCode: async (email, code) => {
console.log("email & code:", email, code)
// await Email.send(email, code)
await Email.send(email, code)
},
}),
),
@@ -149,8 +150,8 @@ export default {
if (value.provider === "password") {
const email = value.email
const username = value.username
const token = await User.create(email)
const usr = await User.fromEmail(email);
const token = await Users.create(email)
const usr = await Users.fromEmail(email);
const exists = await Profiles.getProfile(usr.id)
if(username && !exists){
await Profiles.create({ owner: usr.id, username })
@@ -168,19 +169,17 @@ export default {
if (value.provider === "github") {
const access = value.tokenset.access;
user = await handleGithub(access)
// console.log("user", user)
}
if (value.provider === "discord") {
const access = value.tokenset.access
user = await handleDiscord(access)
// console.log("user", user)
}
if (user) {
try {
const token = await User.create(user.primary.email)
const usr = await User.fromEmail(user.primary.email);
const token = await Users.create(user.primary.email)
const usr = await Users.fromEmail(user.primary.email);
const exists = await Profiles.getProfile(usr.id)
console.log("exists",exists)
if (!exists) {
@@ -198,23 +197,7 @@ export default {
}
// if (email) {
// console.log("email", email)
// // value.username && console.log("username", value.username)
// }
// if (email) {
// const token = await User.create(email);
// const user = await User.fromEmail(email);
// return await ctx.subject("user", {
// accessToken: token,
// userID: user.id
// });
// }
throw new Error("This is not implemented yet");
throw new Error("Something went seriously wrong");
},
}).fetch(request, env, ctx)
}

View File

@@ -260,6 +260,7 @@
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
animation-name: slideFromRight;
}
.modal::backdrop,
.modal-sheet::backdrop {
animation-duration: 0.5s;
@@ -280,7 +281,7 @@
}
.modal[data-closing]::backdrop,
.modal-sheet[data-closing]::backdrop{
.modal-sheet[data-closing]::backdrop {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
touch-action: none;
@@ -298,7 +299,7 @@
animation-name: modalIn;
}
.modal[data-closing]{
.modal[data-closing] {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
touch-action: none;
@@ -325,37 +326,138 @@
@keyframes fadeIn {
from {
opacity: 0;
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
to {
opacity: 0;
}
}
@keyframes modalIn {
from {
opacity: 0;
scale: 0.9;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes modalOut {
to {
opacity: 0;
scale: 0.9;
}
}
/* button, a {
@apply 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)]
} */
to {
opacity: 1;
}
}
@keyframes fadeOut {
to {
opacity: 0;
}
}
@keyframes modalIn {
from {
opacity: 0;
scale: 0.9;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes modalOut {
to {
opacity: 0;
scale: 0.9;
}
}
[data-component="spinner"] {
--spinner-size: 20px;
--spinner-color: #000;
@media (prefers-color-scheme: dark) {
--spinner-color: #FFF;
}
height: var(--spinner-size, 20px);
width: var(--spinner-size, 20px);
margin-left: calc(var(--spinner-size, 20px)*-1px);
/* display: none; */
&>div {
position: relative;
top: 50%;
left: 50%;
height: var(--spinner-size, 20px);
width: var(--spinner-size, 20px);
}
&>div>div {
animation: spin 1.2s linear infinite;
background: var(--spinner-color);
border-radius: 9999px;
height: 8%;
left: -10%;
position: absolute;
top: -3.9%;
width: 24%;
}
&>div>div:first-child {
animation-delay: -1.2s;
transform: rotate(.0001deg) translate(146%);
}
&>div>div:nth-child(2) {
animation-delay: -1.1s;
transform: rotate(30deg) translate(146%);
}
&>div>div:nth-child(3) {
animation-delay: -1s;
transform: rotate(60deg) translate(146%);
}
&>div>div:nth-child(4) {
animation-delay: -.9s;
transform: rotate(90deg) translate(146%);
}
&>div>div:nth-child(5) {
animation-delay: -.8s;
transform: rotate(120deg) translate(146%);
}
&>div>div:nth-child(6) {
animation-delay: -.7s;
transform: rotate(150deg) translate(146%);
}
&>div>div:nth-child(7) {
animation-delay: -.6s;
transform: rotate(180deg) translate(146%);
}
&>div>div:nth-child(8) {
animation-delay: -.5s;
transform: rotate(210deg) translate(146%);
}
&>div>div:nth-child(9) {
animation-delay: -.4s;
transform: rotate(240deg) translate(146%);
}
&>div>div:nth-child(10) {
animation-delay: -.3s;
transform: rotate(270deg) translate(146%);
}
&>div>div:nth-child(11) {
animation-delay: -.2s;
transform: rotate(300deg) translate(146%);
}
&>div>div:nth-child(12) {
animation-delay: -.1s;
transform: rotate(330deg) translate(146%);
}
}
@keyframes spin {
0% {
opacity: 1;
}
100% {
opacity: .15;
}
}

View File

@@ -28,7 +28,7 @@
"@fontsource/bricolage-grotesque": "^5.0.7",
"@fontsource/geist-mono": "^5.1.0",
"@fontsource/geist-sans": "^5.1.0",
"@modular-forms/qwik": "0.26.1",
"@modular-forms/qwik": "^0.29.0",
"@nestri/core": "*",
"@qwik-ui/headless": "^0.6.4",
"@types/eslint": "^8.56.5",

View File

@@ -0,0 +1,4 @@
export const CONSTANTS = {
githubLink: "https://github.com/nestrilabs/nestri#start",
enterpriseContact: "mailto:enterprise@nestri.io"
}

View File

@@ -1,23 +1,26 @@
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import { MotionComponent, transition } from "@nestri/ui/react";
import Book from "./book";
import { CONSTANTS } from "./constants";
import { Link } from "@builder.io/qwik-city";
import { MotionComponent, transition } from "@nestri/ui/react";
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
export const GithubBanner = component$(() => {
const buttonRef = useSignal<HTMLButtonElement | undefined>()
const bookRef = useSignal<HTMLButtonElement | undefined>()
export const FooterBanner = component$(() => {
const docsLinkRef = useSignal<HTMLElement | undefined>()
const bookRef = useSignal<HTMLElement | undefined>()
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(() => {
buttonRef.value?.addEventListener("mouseenter", () => {
docsLinkRef.value?.addEventListener("mouseenter", () => {
bookRef.value?.classList.add('flip')
})
buttonRef.value?.addEventListener("mouseleave", () => {
docsLinkRef.value?.addEventListener("mouseleave", () => {
bookRef.value?.classList.remove('flip')
})
return () => {
buttonRef.value?.removeEventListener("mouseenter", () => {
docsLinkRef.value?.removeEventListener("mouseenter", () => {
bookRef.value?.classList.add('flip')
})
buttonRef.value?.removeEventListener("mouseleave", () => {
docsLinkRef.value?.removeEventListener("mouseleave", () => {
bookRef.value?.classList.remove('flip')
})
}
@@ -41,36 +44,36 @@ export const GithubBanner = component$(() => {
<p class="text-lg font-medium text-balance tracking-tight leading-tight">
<b class="text-black dark:text-white font-semibold text-2xl text-balance tracking-[-.96px] leading-tight font-title">Ready to start playing?</b>
<br />
Dive into the documentation or unlock premium features with <u class="font-bold [text-decoration:none]" >Nestri Family</u>
Dive into the documentation or unlock premium features with <u class="font-bold [text-decoration:none]" >Nestri Pro</u>
</p>
</div>
<div class="flex md:flex-row flex-col w-full gap-2 h-max md:items-center">
<button class="h-max w-max relative overflow-hidden rounded-lg flex justify-center text-gray-500 dark:text-gray-100/70 font-title font-bold items-center group py-2 px-4">
<span class="invisible"> Get Nestri Family</span>
<Link href="/pricing" class="h-max w-max relative overflow-hidden rounded-lg flex justify-center text-gray-500 dark:text-gray-100/70 font-title font-bold items-center group py-2 px-4">
<span class="invisible"> Get Nestri Pro</span>
<div class="animate-multicolor before:-z-[1] -z-[2] absolute -right-full left-0 bottom-0 h-full w-[1000px] [background:linear-gradient(90deg,rgb(232,23,98)_1.26%,rgb(30,134,248)_18.6%,rgb(91,108,255)_34.56%,rgb(52,199,89)_49.76%,rgb(245,197,5)_64.87%,rgb(236,62,62)_85.7%)_0%_0%/50%_100%_repeat-x]" />
<div class="select-none absolute justify-center items-center min-w-max inset-auto flex z-[2] rounded-md h-[83%] w-[96%] bg-white dark:bg-black group-hover:bg-transparent transition-all duration-200">
<span class="text-sm group-hover:text-white w-full transition-all duration-200">
<div class="flex justify-around items-center w-full h-max">
Get Nestri Family
Get Nestri Pro
</div>
</span>
</div>
</button>
<button ref={v => buttonRef.value = v} class="w-max focus:ring-primary-500 hover:ring-primary-500 ring-gray-500 rounded-lg outline-none dark:text-gray-100/70 ring-2 text-sm h-max py-2 px-4 flex items-center transition-all duration-200 focus:bg-primary-100 focus:dark:bg-primary-900 focus:text-primary-500 text-gray-500 font-title font-bold justify-between">
</Link>
<a href={CONSTANTS.githubLink} ref={v => docsLinkRef.value = v} class="w-max focus:ring-primary-500 hover:ring-primary-500 ring-gray-500 rounded-lg outline-none dark:text-gray-100/70 ring-2 text-sm h-max py-2 px-4 flex items-center transition-all duration-200 focus:bg-primary-100 focus:dark:bg-primary-900 focus:text-primary-500 text-gray-500 font-title font-bold justify-between">
<div class="size-5 relative mr-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" height={20} width={20} viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M14.25 4.48v3.057c0 .111 0 .27.021.406a.94.94 0 0 0 .444.683a.96.96 0 0 0 .783.072c.13-.04.272-.108.378-.159L17 8.005l1.124.534c.106.05.248.119.378.16a.96.96 0 0 0 .783-.073a.94.94 0 0 0 .444-.683c.022-.136.021-.295.021-.406V3.031q.17-.008.332-.013C21.154 2.98 22 3.86 22 4.933v11.21c0 1.112-.906 2.01-2.015 2.08c-.97.06-2.108.179-2.985.41c-1.082.286-2.373.904-3.372 1.436q-.422.224-.878.323V5.174a3.6 3.6 0 0 0 .924-.371q.277-.162.576-.323m5.478 8.338a.75.75 0 0 1-.546.91l-4 1a.75.75 0 1 1-.364-1.456l4-1a.75.75 0 0 1 .91.546M11.25 5.214a3.4 3.4 0 0 1-.968-.339C9.296 4.354 8.05 3.765 7 3.487c-.887-.233-2.041-.352-3.018-.412C2.886 3.008 2 3.9 2 4.998v11.146c0 1.11.906 2.01 2.015 2.079c.97.06 2.108.179 2.985.41c1.081.286 2.373.904 3.372 1.436q.422.224.878.324zM4.273 8.818a.75.75 0 0 1 .91-.546l4 1a.75.75 0 1 1-.365 1.456l-4-1a.75.75 0 0 1-.545-.91m.91 3.454a.75.75 0 1 0-.365 1.456l4 1a.75.75 0 0 0 .364-1.456z" clip-rule="evenodd" /><path fill="currentColor" d="M18.25 3.151c-.62.073-1.23.18-1.75.336a8 8 0 0 0-.75.27v3.182l.75-.356l.008-.005a1.1 1.1 0 0 1 .492-.13q.072 0 .138.01c.175.029.315.1.354.12l.009.005l.75.356V3.15" /></svg>
</div>
Read the Docs
</button>
</a>
</div>
</div>
</div>
<button ref={v => bookRef.value = v} class="h-full max-h-[160px] pt-4 md:w-[65%] w-full flex items-start justify-center overflow-hidden outline-none">
<a href={CONSTANTS.githubLink} ref={v => bookRef.value = v} class="h-full max-h-[160px] pt-4 md:w-[65%] w-full flex items-start justify-center overflow-hidden outline-none">
<Book
textColor="#FFF"
bgColor="#FF4F01"
title="Getting started with Nestri" class="shadow-lg shadow-gray-900 dark:shadow-gray-300" />
</button>
</a>
<div class="animate-multicolor absolute blur-[2px] -right-full left-0 -bottom-[2px] h-4 [background:linear-gradient(90deg,rgb(232,23,98)_1.26%,rgb(30,134,248)_18.6%,rgb(91,108,255)_34.56%,rgb(52,199,89)_49.76%,rgb(245,197,5)_64.87%,rgb(236,62,62)_85.7%)_0%_0%/50%_100%_repeat-x]" />
</div>
</div>

View File

@@ -2,7 +2,7 @@
import { component$ } from "@builder.io/qwik";
import { Link } from "@builder.io/qwik-city";
import { MotionComponent, transition } from "@nestri/ui/react"
import { GithubBanner } from "./github-banner";
import { FooterBanner } from "./footer-banner";
const socialMedia = [
{
@@ -30,13 +30,13 @@ const socialMedia = [
]
type Props = {
showGH?: boolean
showBanner?: boolean
}
export const Footer = component$(({ showGH = true }: Props) => {
export const Footer = component$(({ showBanner = true }: Props) => {
return (
<>
{showGH && <GithubBanner />}
{showBanner && <FooterBanner />}
<footer class="flex justify-center flex-col items-center w-full pt-8 sm:pb-0 pb-8 [&>*]:w-full px-3">
<MotionComponent
initial={{ opacity: 0, y: 50 }}
@@ -70,14 +70,16 @@ export const Footer = component$(({ showGH = true }: Props) => {
<div class="text-gray-950/50 dark:text-gray-50/50 flex flex-col gap-2" >
<p class="text-base opacity-50 cursor-not-allowed" >Docs</p>
<Link href="/pricing" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >Pricing</Link>
<Link href="/changelog" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >Changelog</Link>
<Link href="/about" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >About Us</Link>
</div>
</div>
<div class="flex flex-col gap-2">
<h2 class="font-title text-sm font-bold" >Company</h2>
<div class="text-gray-950/50 dark:text-gray-50/50 flex flex-col gap-2" >
<Link href="/blog" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >Blog</Link>
<Link href="/contact" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >Contact Us</Link>
{/* <Link href="/blog" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >Blog</Link> */}
{/* <Link href="/contact" class="text-base hover:text-gray-950 dark:hover:text-gray-50 transition-all duration-200 hover:underline hover:underline-offset-4" >Contact Us</Link> */}
<p class="text-base opacity-50 cursor-not-allowed" >Blog</p>
<p class="text-base opacity-50 cursor-not-allowed" >Contact Us</p>
<p class="text-base opacity-50 cursor-not-allowed" >Open Startup</p>
</div>
</div>

View File

@@ -0,0 +1,89 @@
import { Modal } from "@qwik-ui/headless";
import { MotionComponent } from "../react";
import { component$, type QRL } from "@builder.io/qwik";
// const FadeScale = {
// initial: {
// opacity: 0,
// scale: 0.85,
// },
// animate: {
// opacity: 1,
// scale: 1,
// },
// exit: {
// opacity: 0,
// scale: 0.85,
// },
// transition: {
// type: "spring",
// duration: 0.35,
// bounce: 0.1
// }
// };
type StoreSelectProps = {
onSteamPress$: QRL<() => void>
}
export const StoreSelect = component$(({ onSteamPress$ }: StoreSelectProps) => {
return (
// <MotionComponent class="w-full h-full" key="store-select" {...FadeScale} >
<>
<div class="absolute right-6 top-6 z-10">
<Modal.Close class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full hover:bg-gray-300 transition-all duration-300 ease-out dark:hover:bg-gray-800 focus:scale-90 active:scale-75">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="h-5 w-5 stroke-gray-600 will-change-transform"><path d="M18 6L6 18M6 6L18 18" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</Modal.Close>
</div>
<div >
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
<h2 class="tracking-tight flex items-center justify-between px-6 py-6 text-center font-title text-xl font-semibold">
<div class="flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-full hover:bg-gray-300 transition-all duration-300 ease-out dark:hover:bg-gray-800 focus:scale-90 active:scale-75">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 48 48" class="size-5 text-gray-600"><g fill="none"><path stroke="currentColor" stroke-linejoin="round" stroke-width="4" d="M24 44a19.94 19.94 0 0 0 14.142-5.858A19.94 19.94 0 0 0 44 24a19.94 19.94 0 0 0-5.858-14.142A19.94 19.94 0 0 0 24 4A19.94 19.94 0 0 0 9.858 9.858A19.94 19.94 0 0 0 4 24a19.94 19.94 0 0 0 5.858 14.142A19.94 19.94 0 0 0 24 44Z" /><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M24 28.625v-4a6 6 0 1 0-6-6" /><path fill="currentColor" fill-rule="evenodd" d="M24 37.625a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5" clip-rule="evenodd" /></g></svg>
</div>
<span class="w-full pr-8 text-center font-title font-semibold">Connect Game Store</span>
</h2>
</div>
<div class="flex flex-col gap-3 px-6 pb-4">
<button onClick$={onSteamPress$} class="flex h-[64px] w-full cursor-pointer flex-row items-center justify-between gap-2 rounded-2xl bg-gray-100/70 px-5 transition-all duration-200 ease-out hover:bg-gray-300 focus:bg-gray-300 dark:bg-[#171717] dark:hover:bg-gray-800">
<span class="select-none text-center font-title text-lg font-semibold sm:font-medium">Steam</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16" class="size-8 dark:text-white"><g fill="currentColor"><path d="M.329 10.333A8.01 8.01 0 0 0 7.99 16C12.414 16 16 12.418 16 8s-3.586-8-8.009-8A8.006 8.006 0 0 0 0 7.468l.003.006l4.304 1.769A2.2 2.2 0 0 1 5.62 8.88l1.96-2.844l-.001-.04a3.046 3.046 0 0 1 3.042-3.043a3.046 3.046 0 0 1 3.042 3.043a3.047 3.047 0 0 1-3.111 3.044l-2.804 2a2.223 2.223 0 0 1-3.075 2.11a2.22 2.22 0 0 1-1.312-1.568L.33 10.333Z" /><path d="M4.868 12.683a1.715 1.715 0 0 0 1.318-3.165a1.7 1.7 0 0 0-1.263-.02l1.023.424a1.261 1.261 0 1 1-.97 2.33l-.99-.41a1.7 1.7 0 0 0 .882.84Zm3.726-6.687a2.03 2.03 0 0 0 2.027 2.029a2.03 2.03 0 0 0 2.027-2.029a2.03 2.03 0 0 0-2.027-2.027a2.03 2.03 0 0 0-2.027 2.027m2.03-1.527a1.524 1.524 0 1 1-.002 3.048a1.524 1.524 0 0 1 .002-3.048" /></g></svg>
</button>
<div class="flex h-[64px] w-full cursor-pointer flex-row items-center justify-between gap-2 rounded-2xl bg-gray-100/70 px-5 transition-all duration-200 ease-out hover:bg-gray-300 focus:bg-gray-300 dark:bg-[#171717] dark:hover:bg-gray-800">
<span class="select-none text-center font-title text-lg font-semibold sm:font-medium">Epic Games</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="size-8 text-black dark:text-white" viewBox="0 0 24 24" ><path fill="currentColor" d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4 4 0 0 0 .02.433c.031.3.037.59.316.92c.027.033.311.245.311.245c.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2c0 0 .284-.211.311-.243c.28-.33.285-.621.316-.92a4 4 0 0 0 .02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.7 1.7 0 0 1 .591.108a1.8 1.8 0 0 1 .49.299l-.452.546a1.3 1.3 0 0 0-.308-.195a.9.9 0 0 0-.363-.068a.7.7 0 0 0-.28.06a.7.7 0 0 0-.224.163a.8.8 0 0 0-.151.243a.8.8 0 0 0-.056.299v.008a.9.9 0 0 0 .056.31a.7.7 0 0 0 .157.245a.7.7 0 0 0 .238.16a.8.8 0 0 0 .303.058a.8.8 0 0 0 .445-.116v-.339h-.548v-.565H7.37v1.255a2 2 0 0 1-.524.307a1.8 1.8 0 0 1-.683.123a1.6 1.6 0 0 1-.602-.107a1.5 1.5 0 0 1-.478-.3a1.4 1.4 0 0 1-.318-.455a1.4 1.4 0 0 1-.115-.58v-.008a1.4 1.4 0 0 1 .113-.57a1.5 1.5 0 0 1 .312-.46a1.4 1.4 0 0 1 .474-.309a1.6 1.6 0 0 1 .598-.111h.045zm11.963.008a2 2 0 0 1 .612.094a1.6 1.6 0 0 1 .507.277l-.386.546a1.6 1.6 0 0 0-.39-.205a1.2 1.2 0 0 0-.388-.07a.35.35 0 0 0-.208.052a.15.15 0 0 0-.07.127v.008a.16.16 0 0 0 .022.084a.2.2 0 0 0 .076.066a1 1 0 0 0 .147.06q.093.03.236.061a3 3 0 0 1 .43.122a1.3 1.3 0 0 1 .328.17a.7.7 0 0 1 .207.24a.74.74 0 0 1 .071.337v.008a.9.9 0 0 1-.081.382a.8.8 0 0 1-.229.285a1 1 0 0 1-.353.18a1.6 1.6 0 0 1-.46.061a2.2 2.2 0 0 1-.71-.116a1.7 1.7 0 0 1-.593-.346l.43-.514q.416.335.9.335a.46.46 0 0 0 .236-.05a.16.16 0 0 0 .082-.142v-.008a.15.15 0 0 0-.02-.077a.2.2 0 0 0-.073-.066a1 1 0 0 0-.143-.062a3 3 0 0 0-.233-.062a5 5 0 0 1-.413-.113a1.3 1.3 0 0 1-.331-.16a.7.7 0 0 1-.222-.243a.73.73 0 0 1-.082-.36v-.008a.9.9 0 0 1 .074-.359a.8.8 0 0 1 .214-.283a1 1 0 0 1 .34-.185a1.4 1.4 0 0 1 .448-.066zm-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08l.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z" /></svg>
</div>
<div class="flex h-[64px] w-full cursor-pointer flex-row items-center justify-between gap-2 rounded-2xl bg-gray-100/70 px-5 transition-all duration-200 ease-out hover:bg-gray-300 focus:bg-gray-300 dark:bg-[#171717] dark:hover:bg-gray-800">
<span class="select-none text-center font-title text-lg font-semibold sm:font-medium">GOG.com</span>
<svg preserveAspectRatio="xMidYMax meet" viewBox="0 0 34 31" width="24" height="24" class="size-8 text-black dark:text-white" >
<path fill="currentColor" d="M31,31H3a3,3,0,0,1-3-3V3A3,3,0,0,1,3,0H31a3,3,0,0,1,3,3V28A3,3,0,0,1,31,31ZM4,24.5A1.5,1.5,0,0,0,5.5,26H11V24H6.5a.5.5,0,0,1-.5-.5v-3a.5.5,0,0,1,.5-.5H11V18H5.5A1.5,1.5,0,0,0,4,19.5Zm8-18A1.5,1.5,0,0,0,10.5,5h-5A1.5,1.5,0,0,0,4,6.5v5A1.5,1.5,0,0,0,5.5,13H9V11H6.5a.5.5,0,0,1-.5-.5v-3A.5.5,0,0,1,6.5,7h3a.5.5,0,0,1,.5.5v6a.5.5,0,0,1-.5.5H4v2h6.5A1.5,1.5,0,0,0,12,14.5Zm0,13v5A1.5,1.5,0,0,0,13.5,26h5A1.5,1.5,0,0,0,20,24.5v-5A1.5,1.5,0,0,0,18.5,18h-5A1.5,1.5,0,0,0,12,19.5Zm9-13A1.5,1.5,0,0,0,19.5,5h-5A1.5,1.5,0,0,0,13,6.5v5A1.5,1.5,0,0,0,14.5,13h5A1.5,1.5,0,0,0,21,11.5Zm9,0A1.5,1.5,0,0,0,28.5,5h-5A1.5,1.5,0,0,0,22,6.5v5A1.5,1.5,0,0,0,23.5,13H27V11H24.5a.5.5,0,0,1-.5-.5v-3a.5.5,0,0,1,.5-.5h3a.5.5,0,0,1,.5.5v6a.5.5,0,0,1-.5.5H22v2h6.5A1.5,1.5,0,0,0,30,14.5ZM30,18H22.5A1.5,1.5,0,0,0,21,19.5V26h2V20.5a.5.5,0,0,1,.5-.5h1v6h2V20H28v6h2ZM18.5,11h-3a.5.5,0,0,1-.5-.5v-3a.5.5,0,0,1,.5-.5h3a.5.5,0,0,1,.5.5v3A.5.5,0,0,1,18.5,11Zm-4,9h3a.5.5,0,0,1,.5.5v3a.5.5,0,0,1-.5.5h-3a.5.5,0,0,1-.5-.5v-3A.5.5,0,0,1,14.5,20Z" />
</svg>
</div>
<div class="flex h-[64px] w-full cursor-pointer flex-row items-center justify-between gap-2 rounded-2xl bg-gray-100/70 px-5 transition-all duration-200 ease-out hover:bg-gray-300 focus:bg-gray-300 dark:bg-[#171717] dark:hover:bg-gray-800">
<span class="select-none text-center font-title text-lg font-semibold sm:font-medium">Amazon Games</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="size-8 text-black dark:text-white" viewBox="0 0 291.29 134.46" fill="currentColor">
<path fill-rule="evenodd" d="M50.38,59.78c1.09-3.68,1-8.31,1-13.08V12.56c0-1.64.4-6.32-.25-7.29s-3.15-.75-4.9-.75c-5,0-7.22-.69-7.67,4.08l-.19.13c-3.92-3-7.65-5.85-14.84-5.72l-2.39.19a22.76,22.76,0,0,0-4.08,1A19.69,19.69,0,0,0,6.31,15a36.62,36.62,0,0,0-2.08,7.36,23.87,23.87,0,0,0-.38,7.54c.38,2.36.15,4.42.63,6.48,1.74,7.39,5.21,13.15,11.57,15.9a20.21,20.21,0,0,0,11.13,1.39A21,21,0,0,0,34.35,51l2.7-2h.06A22.54,22.54,0,0,1,37,55.75c-1.12,6.39-3,8.54-9.37,9.68a18,18,0,0,1-5.41.13l-5.28-.69L9.2,63a1.69,1.69,0,0,0-1.26,1.07,40.2,40.2,0,0,0,.25,7.8c.89,1.48,3.75,2.07,5.54,2.64,6,1.91,15.69,2.83,22.19.57C43.36,72.52,48.07,67.51,50.38,59.78ZM37.17,22.87V40.41a15.23,15.23,0,0,1-4.33,2.14c-10.59,3.32-14.59-4.12-14.59-13.89a25.33,25.33,0,0,1,1.13-8.87c.93-2.4,2.37-4.5,4.72-5.47.84-.34,1.85-.26,2.76-.63a21.18,21.18,0,0,1,7.8,1.2L37,16C37.57,17,37.17,21.31,37.17,22.87ZM79.74,56.32a25.65,25.65,0,0,0,8.36-3.21l3.33-2.45c.86,1.11.52,2.8,1.63,3.65s9.68,1.16,10.5,0,.44-3.67.44-5.41V26.46c0-4.37.33-9.26-.69-12.7C100.92,5.67,94.08,2.89,83.51,3l-5.66.37a62,62,0,0,0-9.56,2.08c-1.36.47-3.44.82-4,2.07s-.45,7.84.31,8.36c1.12.77,6.5-1,8-1.32,4.34-.94,14.24-1.9,16.66,1.2C91,18,90.71,22.37,90.67,26.39c-1,.24-2.72-.42-3.77-.63l-4.78-.5a18,18,0,0,0-5.28.19c-8.2,1.41-14,4.53-15.9,12.13C58,49.27,68.13,58.77,79.74,56.32ZM77.35,34.63c1.19-.7,2.67-.51,4.15-1.07,3.35,0,6.18.51,9,.63.51,1.12.14,6.83.12,8.55-2.39,3.17-12,6.33-15.27,1.82C73,41.23,74.57,36.26,77.35,34.63Zm38.53,16c0,1.75-.21,3.48.88,4.15.62.37,2.09.19,3,.19,2.09,0,9.28.44,10.06-.57,1-1.25.44-7.82.44-10.12V16.84a19.35,19.35,0,0,1,6.1-2.27c3.38-.79,7.86-.8,9.55,1.45,1.49,2,1.26,5.56,1.26,9.05v19c0,2.58-.58,9.79.88,10.69.9.54,5,.19,6.41.19s5.54.34,6.42-.32c1.18-.89.69-7.28.69-9.56q0-14.13.06-28.29c.48-.79,2.45-1.11,3.4-1.44,4.14-1.46,10.62-2.42,12.63,1.63,1,2.1.69,5.92.69,9V44.81c0,2.24-.5,8.33.44,9.56.55.71,1.83.57,3.08.57,1.88,0,9.33.33,10.19-.32,1.24-.94.75-4.74.75-6.85V28.22c0-8.24.64-15.75-3-20.44-6.52-8.5-23.71-3.95-30,1.45h-.25C157.15,5.18,153,2.9,146.44,3l-2.64.19a30.21,30.21,0,0,0-5.28,1.19,40.58,40.58,0,0,0-6.35,3l-3.08,1.89c-1.12-1.35-.44-3.54-2-4.46-.61-.37-8.67-.47-9.8-.19a2,2,0,0,0-1.07.69c-.66,1-.32,7.59-.32,9.49Zm96.32,2.13c6.17,3.87,17.31,4.71,26.09,2.52,2.21-.55,6.52-1.33,7.29-3.14a48.27,48.27,0,0,0,.12-7.55,1.83,1.83,0,0,0-.81-.94c-.79-.34-2,.24-2.77.44l-6.48,1.19a23.66,23.66,0,0,1-7.16.26,39.37,39.37,0,0,1-5-.7c-4.92-1.49-8.19-5.16-8.24-11.44,1.17-.53,5-.12,6.6-.12h16c2.3,0,6,.47,7.41-.57,1.89-1.41,1.75-10.85,1.14-13.89-2.07-10.3-8.28-16-20.75-15.78l-1.51.06-4.53.63c-4.86,1.22-9.05,3.46-11.75,6.85a25.69,25.69,0,0,0-3.71,6C201.68,22.42,201,33,203.08,40,204.76,45.59,207.71,49.93,212.2,52.73Zm3.7-32.56c1.13-3.25,3-5.62,6.29-6.66L225,13c7.46-.07,9.52,3.79,9.43,11.26-1,.46-4.25.12-5.66.12H215.21C214.8,23.33,215.58,21.1,215.9,20.17Zm77.65,13.2c-3-5.2-9.52-7.23-15.34-9.62-2.76-1.13-7.28-2.08-7.93-5.28-1.37-6.84,12.69-4.86,16.85-3.83,1.16.28,3.85,1.33,4.59.37s.38-3.29.38-4.77c0-1.23.16-2.8-.32-3.59-.72-1.21-2.61-1.55-4.08-2A36.6,36.6,0,0,0,276,3l-3.59.25A29.08,29.08,0,0,0,265.88,5a14.84,14.84,0,0,0-8,7.79c-2.23,5.52-.14,12.84,3.21,15.53,4,3.23,9.43,5.07,14.58,7.17,2.6,1.06,5.55,1.67,6.1,4.78,1.49,8.45-14.51,5.39-19.3,4.15-1-.27-4.16-1.34-5-.88-1.14.65-.69,3.85-.69,5.59,0,1-.15,2.42.25,3.08,1.2,2,7.83,3.26,10.75,3.84,11.6,2.3,21.92-1.62,25.65-8.93C295.3,43.59,295.64,37,293.55,33.37ZM252.81,83l-2.2.13a37.54,37.54,0,0,0-6.35.69,43.91,43.91,0,0,0-13.52,4.72c-1,.61-5,2.58-4.27,4.4.57,1.46,6.36.25,8.23.12,3.7-.25,5.51-.57,9-.56h6.41a35.9,35.9,0,0,1,5.73.37,8.52,8.52,0,0,1,3.45,1.64c1.46,1.25,1.19,5.49.69,7.48a139.33,139.33,0,0,1-5.78,18.86c-.41,1-3.64,7.3-.06,6.54,1.62-.35,4.9-4,5.91-5.22,5-6.39,8.15-13.75,10.5-23,.54-2.15,1.78-10.6.56-12.57C269.11,83.34,258.52,82.89,252.81,83ZM245,101l-5.72,2.51-9.49,3.58c-8.44,3.27-17.84,5.41-27.23,7.74l-11,2.07-12.95,1.7-4.15.31c-1.66.35-3.61.15-5.47.44a83.4,83.4,0,0,1-12.38.51l-9.37.06-6.73-.25-4.33-.25c-1-.2-2.18-.06-3.27-.26l-13.14-1.44c-3.89-.73-8.07-1-11.76-2l-3.08-.51L93.5,112.65c-8.16-2.55-16.27-4.54-23.89-7.48-8.46-3.27-17.29-6.84-24.77-11.26l-7.41-4.27c-1.35-.81-2.44-2-4.59-2-1.6.79-2.09,1.83-1,3.71a12.73,12.73,0,0,0,2.89,2.83l3.4,3.14c4.9,3.9,9.82,7.91,15.15,11.38,4.6,3,9.5,5.55,14.33,8.36l7.23,3.46c4.13,1.82,8.42,3.7,12.76,5.4l11.13,3.71c6,2,12.53,3,19,4.59l13.64,2,4.4.32,7.42.56h2.7a30.39,30.39,0,0,0,7.92.07l2.83-.07,3.46-.06,11.82-.94c5.3-1.18,10.88-1,15.9-2.52l11.57-2.82a195.36,195.36,0,0,0,20.31-7.11,144.13,144.13,0,0,0,23.63-12.57c2.56-1.72,6.18-3,6.86-6.6C250.75,101.43,247.63,100.27,245,101Z" transform="translate(-3.69 -3)" />
</svg>
</div>
</div>
<div class="flex px-6 pb-4" >
<button class="group flex h-[48px] w-full select-none items-center justify-center gap-2 rounded-full text-base font-semibold dark:text-gray-400/70 text-gray-600/70 transition-all duration-200 ease-out hover:text-gray-800 dark:hover:text-gray-200 focus:scale-95 active:scale-95 sm:font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="size-5 shrink-0 transition-all duration-200 ease-out" 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="m9 15l-2.968 2.968A2.362 2.362 0 0 1 2 16.298V15l1.357-6.784A4 4 0 0 1 7.279 5h9.442a4 4 0 0 1 3.922 3.216L22 15v1.297a2.362 2.362 0 0 1-4.032 1.67L15 15z" /><path d="m9 5l1 2h4l1-2" /></g></svg>
I don't own any games
</button>
</div>
</div>
</>
// </MotionComponent>
)
})
export const SteamLoad = component$(() => {
return (
<MotionComponent key="steam-load">
<div class="h-[200px] w-[240px] bg-red-500">
</div>
</MotionComponent>
)
})

View File

@@ -0,0 +1,34 @@
import { SteamLoad, StoreSelect } from "./default";
import { Modal } from "@qwik-ui/headless";
import { $, component$, useSignal } from "@builder.io/qwik";
export default component$(() => {
const storeSelect = useSignal(true)
return (
<Modal.Root class="w-full" >
<Modal.Trigger class="w-full border-gray-400/70 dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none group transition-all duration-200 border-[2px] h-14 rounded-xl px-4 gap-2 flex items-center justify-between overflow-hidden bg-white dark:bg-black hover:bg-gray-300/70 dark:hover:bg-gray-700/70 disabled:opacity-50">
<div class="py-2 w-2/3 flex flex-col">
<p class="text-text-100 shrink truncate w-full flex">DESKTOP-EUO8VSF</p>
</div>
<div
style={{
"--cutout-avatar-percentage-visible": 0.2,
"--head-margin-percentage": 0.1,
"--size": "3rem"
}}
class="relative h-full flex w-1/3 justify-end">
<img draggable={false} alt="game" width={256} height={256} src="/images/steam.png" class="h-12 shadow-lg shadow-gray-900 ring-gray-400/70 ring-1 bg-black w-12 translate-y-4 rotate-[14deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
</div>
</Modal.Trigger>
<Modal.Panel class="
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[360px] max-h-[75vh] rounded-[28px] border dark:border-[#191918] 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-[#111110]
[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-[#fdfdfc]
backdrop-blur-lg modal" >
<div class="size-full flex flex-col relative text-gray-800 dark:text-gray-200">
{storeSelect.value ? <StoreSelect onSteamPress$={$(() => { console.log("clicked") })} /> : <SteamLoad />}
</div>
</Modal.Panel>
</Modal.Root>
)
})

View File

@@ -1,20 +1,31 @@
import { cn } from "./design";
import Avatar from "./avatar"
import { Dropdown } from '@qwik-ui/headless';
import { MotionComponent } from "./react";
import { Dropdown, Modal } from '@qwik-ui/headless';
import { disablePageScroll, enablePageScroll } from '@fluejs/noscroll';
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
type Props = {
avatarUrl?: string;
discriminator: string | number;
username: string;
}
export const HomeNavBar = component$(() => {
export const HomeNavBar = component$(({ avatarUrl, username, discriminator }: Props) => {
const hasScrolled = useSignal(false);
const defaultTeam = `${username}'s Games`
const selectedTeam = useSignal(defaultTeam)
const isNewTeam = useSignal(false);
const isNewMember = useSignal(false);
const isHolding = useSignal(false);
const showInviteSuccess = useSignal(false);
const newTeamName = useSignal('');
const inviteName = useSignal('');
const inviteEmail = useSignal('');
const teams = useSignal([
{ name: defaultTeam }
]);
const actions = [
{ label: "Hell Diver's Europe", disabled: false },
{ label: "WanjohiRyan's Games", disabled: false },
{ label: "CyberPunk Marathon", disabled: false },
{ label: "Emulation Hackers", disabled: true },
{ label: "testing-123", disabled: false },
];
const onDialogOpen = $((open: boolean) => {
if (open) {
@@ -24,111 +35,213 @@ export const HomeNavBar = component$(() => {
}
})
const handlePointerDown = $(() => {
isHolding.value = true
});
const handlePointerUp = $(() => {
isHolding.value = false
});
const handleAddTeam = $((e: any) => {
e.preventDefault();
if (newTeamName.value.trim()) {
teams.value = [...teams.value, { name: newTeamName.value.trim() }];
// selectedTeam.value = newTeamName.value.trim()
newTeamName.value = '';
isNewTeam.value = false;
}
});
const handleInvite = $((e: any) => {
e.preventDefault();
if (inviteName.value && inviteEmail.value) {
// Here you would typically make an API call to send the invitation
console.log('Sending invite to:', { name: inviteName.value, email: inviteEmail.value });
inviteName.value = '';
inviteEmail.value = '';
isNewMember.value = false;
showInviteSuccess.value = true;
setTimeout(() => {
showInviteSuccess.value = false;
}, 3000);
}
});
useOnDocument(
'scroll',
$(() => {
hasScrolled.value = window.scrollY > 0;
})
);
//
const handleDeleteTeam = $(() => {
// Only delete if it's not the default team
if (selectedTeam.value !== defaultTeam) {
teams.value = teams.value.filter(team => team.name !== selectedTeam.value);
selectedTeam.value = defaultTeam;
}
});
const handleDeleteAnimationComplete = $(() => {
if (isHolding.value) {
// isDeleting.value = true;
// Reset the holding state
isHolding.value = false;
handleDeleteTeam();
}
});
return (
<nav class={cn("fixed w-screen justify-between top-0 z-50 px-2 sm:px-6 text-xs sm:text-sm leading-[1] text-gray-950/70 dark:text-gray-50/70 h-[66px] before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full flex items-center", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-300 dark:shadow-gray-700")} >
<div class="flex flex-row justify-center relative items-center top-0 bottom-0">
<div class="flex-shrink-0 gap-2 flex justify-center items-center">
<svg
class="size-8 "
width="100%"
height="100%"
viewBox="0 0 12.8778 9.7377253"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg">
<path
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
</svg>
<>
<nav class={cn("fixed w-screen justify-between top-0 z-50 px-2 sm:px-6 text-xs sm:text-sm leading-[1] text-gray-950/70 dark:text-gray-50/70 h-[66px] before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full flex items-center", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-300 dark:shadow-gray-700")} >
<div class="flex flex-row justify-center relative items-center top-0 bottom-0">
<div class="flex-shrink-0 gap-2 flex justify-center items-center">
<svg
class="size-8 "
width="100%"
height="100%"
viewBox="0 0 12.8778 9.7377253"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg">
<path
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
</svg>
</div>
<div class="relative z-[5] animate-fade-in opacity-0 items-center flex">
<hr class="dark:bg-gray-700/70 bg-gray-400/70 w-0.5 rounded-md mx-3 rotate-[16deg] h-7 border-none" />
<Dropdown.Root onOpenChange$={onDialogOpen}>
<Dropdown.Trigger class="text-sm [&>svg:first-child]:size-5 rounded-full h-8 focus:bg-gray-300/70 dark:focus:bg-gray-700/70 focus:ring-[#8f8f8f] dark:focus:ring-[#707070] focus:ring-2 outline-none dark:text-gray-400 text-gray-600 gap-2 px-3 cursor-pointer inline-flex transition-all duration-150 items-center hover:bg-gray-300/70 dark:hover:bg-gray-700/70 ">
<Avatar name={selectedTeam.value} />
<span class="truncate shrink max-w-[20ch]">{selectedTeam.value}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M72.61 83.06a8 8 0 0 1 1.73-8.72l48-48a8 8 0 0 1 11.32 0l48 48A8 8 0 0 1 176 88H80a8 8 0 0 1-7.39-4.94M176 168H80a8 8 0 0 0-5.66 13.66l48 48a8 8 0 0 0 11.32 0l48-48A8 8 0 0 0 176 168" /></svg>
</Dropdown.Trigger>
<Dropdown.Popover
class="bg-[hsla(0,0%,100%,.5)] dark:bg-[hsla(0,0%,100%,.026)] min-w-[160px] max-w-[240px] backdrop-blur-md rounded-lg py-1 px-2 border border-[#e8e8e8] dark:border-[#2e2e2e] [box-shadow:0_8px_30px_rgba(0,0,0,.12)]">
<Dropdown.RadioGroup onChange$={(v: string) => selectedTeam.value = v} value={selectedTeam.value} class="w-full flex overflow-hidden flex-col gap-1 [&_*]:w-full [&_[data-checked]]:bg-[rgba(0,0,0,.071)] dark:[&_[data-checked]]:bg-[hsla(0,0%,100%,.077)] [&_[data-checked]]:rounded-md [&_[data-checked]]:text-[#171717] [&_[data-checked]_svg]:block cursor-pointer [&_[data-highlighted]]:text-[#171717] dark:[&_[data-checked]]:text-[#ededed] dark:[&_[data-highlighted]]:text-[#ededed] [&_[data-highlighted]]:bg-[rgba(0,0,0,.071)] dark:[&_[data-highlighted]]:bg-[hsla(0,0%,100%,.077)] [&_[data-highlighted]]:rounded-md">
{teams.value.map((team) => (
<Dropdown.RadioItem
key={team.name}
value={team.name}
class="leading-none text-sm items-center flex px-2 h-8 rounded-md outline-none relative select-none w-full"
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate [&>svg]:size-5 ">
<Avatar class="flex-shrink-0 rounded-full" name={team.name} />
{team.name}
</span>
<span class="py-1 px-1 text-primary-500 [&>svg]:size-5 [&>svg]:hidden !w-max" >
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 24 24"><path fill="currentColor" d="m10 13.6l5.9-5.9q.275-.275.7-.275t.7.275t.275.7t-.275.7l-6.6 6.6q-.3.3-.7.3t-.7-.3l-2.6-2.6q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275z" /></svg>
</span>
</Dropdown.RadioItem>
))}
</Dropdown.RadioGroup>
<Dropdown.Separator class="w-full dark:bg-[#2e2e2e] bg-[#e8e8e8] border-0 h-[1px] my-1" />
<Dropdown.Group class="flex flex-col gap-1 w-full">
<Dropdown.Item
onClick$={() => isNewTeam.value = true}
class="leading-none w-full text-sm items-center text-[#6f6f6f] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative"
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14" /></svg>
New Team
</span>
</Dropdown.Item>
<Dropdown.Item
onClick$={() => isNewMember.value = true}
class={cn("leading-none w-full text-sm items-center text-[#6f6f6f] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative", selectedTeam.value === defaultTeam && "opacity-50 pointer-events-none !cursor-not-allowed")}
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0m8 12h6m-3-3v6M6 21v-2a4 4 0 0 1 4-4h4" /></svg>
Send an invite
</span>
</Dropdown.Item>
<button
onPointerDown$={handlePointerDown}
onPointerUp$={handlePointerUp}
onPointerLeave$={handlePointerUp}
onKeyDown$={(e) => e.key === "Enter" && handlePointerDown()}
onKeyUp$={(e) => e.key === "Enter" && handlePointerUp()}
disabled={selectedTeam.value === defaultTeam}
class={cn("leading-none relative overflow-hidden transition-all duration-200 text-sm group items-center text-red-500 [&>*]:w-full [&>qwik-react]:absolute [&>qwik-react]:h-full [&>qwik-react]:left-0 hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none select-none w-full", selectedTeam.value === defaultTeam && "opacity-50 pointer-events-none !cursor-not-allowed")}>
<MotionComponent
client:load
class="absolute left-0 top-0 bottom-0 bg-red-500 opacity-50 w-full h-full rounded-md"
initial={{ scaleX: 0 }}
animate={{
scaleX: isHolding.value ? 1 : 0
}}
style={{
transformOrigin: 'left',
}}
transition={{
duration: isHolding.value ? 2 : 0.5,
ease: "linear"
}}
onAnimationComplete$={handleDeleteAnimationComplete}
/>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5 ">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m19.5 5.5l-.402 6.506M4.5 5.5l.605 10.025c.154 2.567.232 3.85.874 4.774c.317.456.726.842 1.2 1.131c.671.41 1.502.533 2.821.57m10-7l-7 7m7 0l-7-7M3 5.5h18m-4.944 0l-.683-1.408c-.453-.936-.68-1.403-1.071-1.695a2 2 0 0 0-.275-.172C13.594 2 13.074 2 12.035 2c-1.066 0-1.599 0-2.04.234a2 2 0 0 0-.278.18c-.395.303-.616.788-1.058 1.757L8.053 5.5" color="currentColor" /></svg>
<span class="group-hover:hidden">Delete Team</span>
<span class="hidden group-hover:block">Hold to delete</span>
</span>
</button>
</Dropdown.Group>
</Dropdown.Popover>
</Dropdown.Root>
</div>
</div>
<div class="relative z-[5] animate-fade-in opacity-0 items-center flex">
<hr class="dark:bg-gray-700/70 bg-gray-400/70 w-0.5 rounded-md mx-3 rotate-[16deg] h-7 border-none" />
<div class="gap-4 flex flex-row justify-center h-full animate-fade-in opacity-0 items-center">
<Dropdown.Root onOpenChange$={onDialogOpen}>
<Dropdown.Trigger class="text-sm [&>svg:first-child]:size-5 rounded-full h-8 focus:bg-gray-300/70 dark:focus:bg-gray-700/70 focus:ring-[#8f8f8f] dark:focus:ring-[#707070] focus:ring-2 outline-none dark:text-gray-400 text-gray-600 gap-2 px-3 cursor-pointer inline-flex transition-all duration-150 items-center hover:bg-gray-300/70 dark:hover:bg-gray-700/70 ">
<Avatar name="WanjohiRyan's Games" />
<span class="truncate shrink max-w-[20ch]">WanjohiRyan's Games</span>
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M72.61 83.06a8 8 0 0 1 1.73-8.72l48-48a8 8 0 0 1 11.32 0l48 48A8 8 0 0 1 176 88H80a8 8 0 0 1-7.39-4.94M176 168H80a8 8 0 0 0-5.66 13.66l48 48a8 8 0 0 0 11.32 0l48-48A8 8 0 0 0 176 168" /></svg>
<Dropdown.Trigger class="focus:bg-gray-300/70 dark:focus:bg-gray-700/70 focus:ring-[#8f8f8f] dark:focus:ring-[#707070] text-gray-600 dark:text-gray-400 [&>svg:first-child]:size-5 text-sm focus:ring-2 outline-none rounded-full transition-all flex items-center duration-150 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70 gap-1 px-3 h-8" >
{avatarUrl ? (<img src={avatarUrl} height={20} width={20} class="size-6 rounded-full" alt="Avatar" />) : (<Avatar name={`${username}#${discriminator}`} />)}
<span class="truncate shrink max-w-[20ch] sm:flex hidden">{`${username}#${discriminator}`}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="size-4 sm:block hidden" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M72.61 83.06a8 8 0 0 1 1.73-8.72l48-48a8 8 0 0 1 11.32 0l48 48A8 8 0 0 1 176 88H80a8 8 0 0 1-7.39-4.94M176 168H80a8 8 0 0 0-5.66 13.66l48 48a8 8 0 0 0 11.32 0l48-48A8 8 0 0 0 176 168" /></svg>
</Dropdown.Trigger>
<Dropdown.Popover
class="bg-[hsla(0,0%,100%,.5)] dark:bg-[hsla(0,0%,100%,.026)] min-w-[160px] max-w-[240px] backdrop-blur-md rounded-lg py-1 px-2 border border-[#e8e8e8] dark:border-[#2e2e2e] [box-shadow:0_8px_30px_rgba(0,0,0,.12)]">
<Dropdown.Group class="flex flex-col gap-1">
{actions.map((action) => (
<Dropdown.Item
key={action.label}
class="leading-none text-sm items-center text-[#6f6f6f] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative select-none "
disabled={action.disabled}
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5 ">
<Avatar class="flex-shrink-0 rounded-full" name={action.label} />
{action.label}
</span>
{/* <div class="ml-auto">
<kbd class="[text-shadow:hsla(0,0%,100%,.5)_0_0_1px] gap-1 items-center flex justify-center truncate px-1.5 text-xs min-w-5 h-5 rounded-[4px] bg-[rgba(0,0,0,.047)] dark:bg-[hsla(0,0%,100%,.056)] text-[#6f6f6f] dark:text-[#a0a0a0]">
{key + 1}
</kbd>
</div> */}
</Dropdown.Item>
))}
</Dropdown.Group>
<Dropdown.Separator class="w-full dark:bg-[#2e2e2e] bg-[#e8e8e8] border-0 h-[1px] my-1" />
<Dropdown.Group class="flex flex-col gap-1">
<Dropdown.Item
onClick$={() => window.location.href = "mailto:feedback@nestri.io"}
class="leading-none text-sm items-center text-[#6f6f6f] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative select-none "
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5 ">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14" /></svg>
New Team
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="currentColor" d="M22 8.5a6.5 6.5 0 0 0-11.626-3.993A9.5 9.5 0 0 1 19.5 14q0 .165-.006.33l.333.088a1.3 1.3 0 0 0 1.592-1.591l-.128-.476c-.103-.385-.04-.791.125-1.153A6.5 6.5 0 0 0 22 8.5" /><path fill="currentColor" fill-rule="evenodd" d="M18 14a8 8 0 0 1-11.45 7.22a1.67 1.67 0 0 0-1.15-.13l-1.227.329a1.3 1.3 0 0 1-1.591-1.592L2.91 18.6a1.67 1.67 0 0 0-.13-1.15A8 8 0 1 1 18 14M6.5 15a1 1 0 1 0 0-2a1 1 0 0 0 0 2m3.5 0a1 1 0 1 0 0-2a1 1 0 0 0 0 2m3.5 0a1 1 0 1 0 0-2a1 1 0 0 0 0 2" clip-rule="evenodd" /></svg>
Send Feedback
</span>
</Dropdown.Item>
<Dropdown.Item class="leading-none transition-all duration-200 text-sm group items-center text-red-500 hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative select-none">
<button
onPointerDown$={handlePointerDown}
onPointerUp$={handlePointerUp}
onPointerLeave$={handlePointerUp}
onKeyDown$={(e) => e.key === "Enter" && handlePointerDown()}
onKeyUp$={(e) => e.key === "Enter" && handlePointerUp()}
class="leading-none relative overflow-hidden transition-all duration-200 text-sm group items-center text-red-500 [&>*]:w-full [&>qwik-react]:absolute [&>qwik-react]:h-full [&>qwik-react]:left-0 hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none select-none w-full">
<MotionComponent
client:load
class="absolute left-0 top-0 bottom-0 bg-red-500 opacity-50 w-full h-full rounded-md"
initial={{ scaleX: 0 }}
animate={{
scaleX: isHolding.value ? 1 : 0
}}
style={{
transformOrigin: 'left',
}}
transition={{
duration: isHolding.value ? 2 : 0.5,
ease: "linear"
}}
/>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5 ">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m19.5 5.5l-.402 6.506M4.5 5.5l.605 10.025c.154 2.567.232 3.85.874 4.774c.317.456.726.842 1.2 1.131c.671.41 1.502.533 2.821.57m10-7l-7 7m7 0l-7-7M3 5.5h18m-4.944 0l-.683-1.408c-.453-.936-.68-1.403-1.071-1.695a2 2 0 0 0-.275-.172C13.594 2 13.074 2 12.035 2c-1.066 0-1.599 0-2.04.234a2 2 0 0 0-.278.18c-.395.303-.616.788-1.058 1.757L8.053 5.5" color="currentColor" /></svg>
<span class="group-hover:hidden">Delete Team</span>
<span class="hidden group-hover:block">Hold to delete</span>
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><g fill="none"><path fill="currentColor" fill-rule="evenodd" d="M10.138 1.815A3 3 0 0 1 14 4.688v14.624a3 3 0 0 1-3.862 2.873l-6-1.8A3 3 0 0 1 2 17.512V6.488a3 3 0 0 1 2.138-2.873zM15 4a1 1 0 0 1 1-1h3a3 3 0 0 1 3 3v1a1 1 0 1 1-2 0V6a1 1 0 0 0-1-1h-3a1 1 0 0 1-1-1m6 12a1 1 0 0 1 1 1v1a3 3 0 0 1-3 3h-3a1 1 0 1 1 0-2h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1M9 11a1 1 0 1 0 0 2h.001a1 1 0 1 0 0-2z" clip-rule="evenodd" /><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12h5m0 0l-2-2m2 2l-2 2" /></g></svg>
<span class="group-hover:hidden">Log out</span>
<span class="hidden group-hover:block">Hold to logout</span>
</span>
</Dropdown.Item>
</button>
</Dropdown.Group>
</Dropdown.Popover>
</Dropdown.Root>
</div>
</div>
<div class="gap-4 flex flex-row justify-center h-full animate-fade-in opacity-0 items-center">
<Dropdown.Root onOpenChange$={onDialogOpen}>
<Dropdown.Trigger class="focus:bg-gray-300/70 dark:focus:bg-gray-700/70 focus:ring-[#8f8f8f] dark:focus:ring-[#707070] text-gray-600 dark:text-gray-400 [&>svg:first-child]:size-5 text-sm focus:ring-2 outline-none rounded-full transition-all flex items-center duration-150 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70 gap-1 px-3 h-8" >
<img src="https://avatars.githubusercontent.com/u/71614375?v=4" height={20} width={20} class="size-6 rounded-full" alt="Avatar" />
{/* <Avatar name="WanjohiRyan#47" /> */}
<span class="truncate shrink max-w-[20ch] sm:flex hidden">WanjohiRyan#47</span>
<svg xmlns="http://www.w3.org/2000/svg" class="size-4 sm:block hidden" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M72.61 83.06a8 8 0 0 1 1.73-8.72l48-48a8 8 0 0 1 11.32 0l48 48A8 8 0 0 1 176 88H80a8 8 0 0 1-7.39-4.94M176 168H80a8 8 0 0 0-5.66 13.66l48 48a8 8 0 0 0 11.32 0l48-48A8 8 0 0 0 176 168" /></svg>
</Dropdown.Trigger>
<Dropdown.Popover
class="bg-[hsla(0,0%,100%,.5)] dark:bg-[hsla(0,0%,100%,.026)] min-w-[160px] max-w-[240px] backdrop-blur-md rounded-lg py-1 px-2 border border-[#e8e8e8] dark:border-[#2e2e2e] [box-shadow:0_8px_30px_rgba(0,0,0,.12)]">
<Dropdown.Group class="flex flex-col gap-1">
<Dropdown.Item
class="leading-none text-sm items-center text-[#6f6f6f] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative select-none "
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5 ">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="currentColor" d="M22 8.5a6.5 6.5 0 0 0-11.626-3.993A9.5 9.5 0 0 1 19.5 14q0 .165-.006.33l.333.088a1.3 1.3 0 0 0 1.592-1.591l-.128-.476c-.103-.385-.04-.791.125-1.153A6.5 6.5 0 0 0 22 8.5" /><path fill="currentColor" fill-rule="evenodd" d="M18 14a8 8 0 0 1-11.45 7.22a1.67 1.67 0 0 0-1.15-.13l-1.227.329a1.3 1.3 0 0 1-1.591-1.592L2.91 18.6a1.67 1.67 0 0 0-.13-1.15A8 8 0 1 1 18 14M6.5 15a1 1 0 1 0 0-2a1 1 0 0 0 0 2m3.5 0a1 1 0 1 0 0-2a1 1 0 0 0 0 2m3.5 0a1 1 0 1 0 0-2a1 1 0 0 0 0 2" clip-rule="evenodd" /></svg>
Send Feedback
</span>
</Dropdown.Item>
<Dropdown.Item
class="leading-none text-sm items-center text-[#6f6f6f] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative select-none "
>
<span class="w-full max-w-[20ch] flex items-center gap-2 truncate overflow-visible [&>svg]:size-5 ">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><g fill="none"><path fill="currentColor" fill-rule="evenodd" d="M10.138 1.815A3 3 0 0 1 14 4.688v14.624a3 3 0 0 1-3.862 2.873l-6-1.8A3 3 0 0 1 2 17.512V6.488a3 3 0 0 1 2.138-2.873zM15 4a1 1 0 0 1 1-1h3a3 3 0 0 1 3 3v1a1 1 0 1 1-2 0V6a1 1 0 0 0-1-1h-3a1 1 0 0 1-1-1m6 12a1 1 0 0 1 1 1v1a3 3 0 0 1-3 3h-3a1 1 0 1 1 0-2h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1M9 11a1 1 0 1 0 0 2h.001a1 1 0 1 0 0-2z" clip-rule="evenodd" /><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12h5m0 0l-2-2m2 2l-2 2" /></g></svg>
Log out
</span>
</Dropdown.Item>
</Dropdown.Group>
<Dropdown.Separator class="w-full dark:bg-[#2e2e2e] bg-[#e8e8e8] border-0 h-[1px] my-1" />
{/* <Dropdown.Separator class="w-full dark:bg-[#2e2e2e] bg-[#e8e8e8] border-0 h-[1px] my-1" />
<Dropdown.Group class="flex flex-col gap-1">
<Dropdown.Item
class="leading-none transition-all duration-200 text-sm group items-center text-red-500 hover:text-[#171717] dark:hover:text-[#ededed] hover:bg-[rgba(0,0,0,.071)] dark:hover:bg-[hsla(0,0%,100%,.077)] flex px-2 gap-2 h-8 rounded-md cursor-pointer outline-none relative select-none "
@@ -139,10 +252,97 @@ export const HomeNavBar = component$(() => {
<span class="hidden group-hover:block">Hold to leave</span>
</span>
</Dropdown.Item>
</Dropdown.Group>
</Dropdown.Popover>
</Dropdown.Root>
</div>
</nav>
</Dropdown.Group> */}
</Dropdown.Popover>
</Dropdown.Root>
</div>
</nav>
<Modal.Root bind:show={isNewTeam} class="w-full">
<Modal.Panel
class="dark:bg-black bg-white [box-shadow:0_8px_30px_rgba(0,0,0,.12)]
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] max-h-[75vh] rounded-xl
backdrop-blur-md modal max-w-[400px] w-full border dark:border-gray-800 border-gray-200">
<form preventdefault:submit onSubmit$={handleAddTeam}>
<main class="size-full flex flex-col relative py-4 px-5">
<div class="dark:text-white text-black">
<h3 class="font-semibold text-2xl tracking-tight mb-1 font-title">Create a team</h3>
<div class="text-sm dark:text-gray-200/70 text-gray-800/70" >
Continue to start playing with on Pro with increased usage, additional security features, and support
</div>
</div>
<div class="mt-3 flex flex-col gap-3" >
<div>
<label for="name" class="text-sm dark:text-gray-200 text-gray-800 pb-2 pt-1" >
Team Name
</label>
<input
//@ts-expect-error
onInput$={(e) => newTeamName.value = e.target!.value}
required value={newTeamName.value} id="name" type="text" placeholder="Enter team name" class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] w-full bg-transparent px-2 py-3 h-10 border text-black dark:text-white dark:border-gray-700/70 border-gray-300/70 rounded-md text-sm outline-none leading-none focus:ring-gray-300 dark:focus:ring-gray-700 focus:ring-2" />
</div>
</div>
</main>
<footer class="dark:text-gray-200/70 text-gray-800/70 dark:bg-gray-900 bg-gray-100 ring-1 ring-gray-200 dark:ring-gray-800 select-none flex gap-2 items-center justify-between w-full bottom-0 left-0 py-3 px-5 text-sm leading-none">
<Modal.Close class="rounded-lg [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] py-3 px-4 hover:bg-gray-200 dark:hover:bg-gray-800 flex items-center justify-center">
Cancel
</Modal.Close>
<button
type="submit"
class="flex items-center justify-center gap-2 border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-200 dark:bg-gray-800 py-3 px-4 hover:bg-gray-300 dark:hover:bg-gray-700 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)]" >
Continue
</button>
</footer>
</form>
</Modal.Panel>
</Modal.Root >
<Modal.Root bind:show={isNewMember} class="w-full">
<Modal.Panel
class="dark:bg-black bg-white [box-shadow:0_8px_30px_rgba(0,0,0,.12)]
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] max-h-[75vh] rounded-xl
backdrop-blur-md modal max-w-[400px] w-full border dark:border-gray-800 border-gray-200">
<form preventdefault:submit onSubmit$={handleInvite}>
<main class="size-full flex flex-col relative py-4 px-5">
<div class="dark:text-white text-black">
<h3 class="font-semibold text-2xl tracking-tight mb-1 font-title">Send an invite</h3>
<div class="text-sm dark:text-gray-200/70 text-gray-800/70" >
Friends will receive an email allowing them to join this team
</div>
</div>
<div class="mt-3 flex flex-col gap-3" >
<div>
<label for="name" class="text-sm dark:text-gray-200 text-gray-800 pb-2 pt-1" >
Name
</label>
<input
value={inviteName.value}
//@ts-expect-error
onInput$={(e) => inviteName.value = e.target!.value}
id="name" type="text" placeholder="Jane Doe" class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] w-full bg-transparent px-2 py-3 h-10 border text-black dark:text-white dark:border-gray-700/70 border-gray-300/70 rounded-md text-sm outline-none leading-none focus:ring-gray-300 dark:focus:ring-gray-700 focus:ring-2" />
</div>
<div>
<label for="email" class="text-sm dark:text-gray-200 text-gray-800 pb-2 pt-1" >
Email
</label>
<input
value={inviteEmail.value}
//@ts-expect-error
onInput$={(e) => inviteEmail.value = e.target!.value}
id="email" type="email" placeholder="jane@doe.com" class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] w-full px-2 bg-transparent py-3 h-10 border text-black dark:text-white dark:border-gray-700/70 border-gray-300/70 rounded-md text-sm outline-none leading-none focus:ring-gray-300 dark:focus:ring-gray-700 focus:ring-2" />
</div>
</div>
</main>
<footer class="dark:text-gray-200/70 text-gray-800/70 dark:bg-gray-900 bg-gray-100 ring-1 ring-gray-200 dark:ring-gray-800 select-none flex gap-2 items-center justify-between w-full bottom-0 left-0 py-3 px-5 text-sm leading-none">
<Modal.Close class="rounded-lg [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] py-3 px-4 hover:bg-gray-200 dark:hover:bg-gray-800 flex items-center justify-center">
Cancel
</Modal.Close>
<button type="submit" class="flex items-center justify-center gap-2 border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-200 dark:bg-gray-800 py-3 px-4 hover:bg-gray-300 dark:hover:bg-gray-700 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)]" >
Send an invite
</button>
</footer>
</form>
</Modal.Panel>
</Modal.Root>
</>
)
})

View File

@@ -9,9 +9,12 @@ export * from "./footer"
export * from "./card"
export * from "./router-head"
export * from "./team-counter"
export * from "./constants"
export * from "./svg"
export * as auth from "./popup"
export * as Modal from "./modal"
export { default as Book } from "./book"
export { default as Portal } from "./portal"
export { default as Avatar } from "./avatar"
export { default as SimpleFooter } from "./simple-footer"
export { default as SimpleFooter } from "./simple-footer"
export { default as GameStoreButton } from "./game-store"

View File

@@ -4,8 +4,8 @@ import { buttonVariants, cn } from "./design";
const navLinks = [
{
name: "Changelog",
href: "/changelog"
name: "About Us",
href: "/about"
},
{
name: "Pricing",
@@ -13,11 +13,15 @@ const navLinks = [
},
{
name: "Login",
href: "/login"
}
]
export const NavBar = component$(() => {
type Props = {
link?: string
}
export const NavBar = component$(({ link }: Props) => {
const location = useLocation()
const hasScrolled = useSignal(false);
@@ -30,8 +34,16 @@ export const NavBar = component$(() => {
);
return (
<nav class={cn("w-full sticky top-0 z-50 px-4 text-sm font-extrabold bg-gray-100/70 dark:bg-gray-900/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-300 dark:shadow-gray-700")} >
<div class="mx-auto flex max-w-xl items-center border-b-2 dark:border-gray-50/50 border-gray-950/50" >
<nav class={cn("w-full sticky top-0 z-50 text-sm font-extrabold bg-gray-100/70 dark:bg-gray-900/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full max-w-full overflow-hidden", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-300 dark:shadow-gray-700")} >
<button onClick$={() => window.location.href = link as string} class="w-full text-gray-900/70 bg-gray-400/30 dark:bg-gray-600/30 dark:text-gray-100/30 whitespace-nowrap font-mono text-sm py-3">
<div class="flex relative">
<span class="whitespace-pre marquee-animation">
Launching Soon · Login to reserve your username · Launching Soon · Login to reserve your username · Launching Soon · Login to reserve your username ·
Launching Soon · Login to reserve your username · Launching Soon · Login to reserve your username · Launching Soon · Login to reserve your username ·
</span>
</div>
</button>
<div class="px-4 mx-auto flex max-w-xl items-center border-b-2 dark:border-gray-50/50 border-gray-950/50" >
<Link class="outline-none focus:ring-2 py-1 px-3 -ml-3 rounded-lg focus:ring-primary-500 duration-200 transition-all" href="/" >
<h1 class="text-lg font-title" >
Nestri
@@ -40,7 +52,7 @@ export const NavBar = component$(() => {
<ul class="ml-0 -mr-4 flex font-medium m-4 flex-1 gap-1 tracking-[0.035em] items-center justify-end dark:text-primary-50/70 text-primary-950/70">
{navLinks.map((linkItem, key) => (
<li key={`linkItem-${key}`}>
<Link href={linkItem.href} class={cn(buttonVariants.ghost({ intent: "gray", size: "sm" }), "hover:bg-gray-300/70 dark:hover:bg-gray-700/70 focus:ring-2 outline-none focus:ring-primary-500 duration-200 transition-all", location.url.pathname === linkItem.href && "bg-gray-300/70 hover:bg-gray-300/70 dark:bg-gray-700/70 dark:hover:bg-gray-700/70")}>
<Link href={linkItem.href ? linkItem.href : link} class={cn(buttonVariants.ghost({ intent: "gray", size: "sm" }), "hover:bg-gray-300/70 dark:hover:bg-gray-700/70 focus:ring-2 outline-none focus:ring-primary-500 duration-200 transition-all", location.url.pathname === linkItem.href && "bg-gray-300/70 hover:bg-gray-300/70 dark:bg-gray-700/70 dark:hover:bg-gray-700/70")}>
{linkItem.name}
</Link>
</li>

View File

@@ -0,0 +1,32 @@
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { type MotionProps, AnimatePresence } from 'framer-motion';
import React, { type ReactNode } from 'react';
interface ReactAnimateComponentProps extends MotionProps {
// as?: keyof JSX.IntrinsicElements;
children?: ReactNode;
// class?: string;
// id: string;
}
export const ReactAnimateComponent = ({
// as = 'div',
// id,
children,
// class: className,
// ...motionProps
}: ReactAnimateComponentProps) => {
// const MotionTag = motion[as as keyof typeof motion] as React.ComponentType<any>;
return (
<AnimatePresence mode='wait'>
{children}
{/* <MotionTag id={id} className={className} {...(motionProps as any)}>
</MotionTag> */}
</AnimatePresence>
);
};
export const AnimateComponent = qwikify$(ReactAnimateComponent);

View File

@@ -5,4 +5,5 @@ export * from "./button"
export * from "./cursor"
export * from "./motion"
export * from "./title"
export * from "./text"
export * from "./text"
export * from "./animate"

11
packages/ui/src/svg.tsx Normal file

File diff suppressed because one or more lines are too long